2025-07-26 Line 訂單系統- 先做「NestJS 第一個練習(Products 模組)」 先做「NestJS 第一個練習(Products 模組)」
我會一步步帶你從 0 建出 可運行、可擴充的 NestJS 專案骨架 ,並把 Products Module(含 Controller/Service/DTO/Entity/DI) 實作好,讓你能像「堆積木」一樣繼續往 Orders、Webhook、Redis 等積木擴充。
下面的流程分兩部分:
建立專案骨架(用 Nest CLI)
把 Products Module 的所有檔案都寫好(可直接執行)
我會給你完整命令與每個檔案的程式碼——你只要複製貼上即可啟動。
0) 前置:環境需求 確保你系統有 Node.js(>=16)、npm/yarn。 建議用 npm。
1) 建立 NestJS 專案(骨架) 在終端機執行:
1 2 3 4 5 6 npm install -g @nestjs/cli nest new line-order-backend
進入專案目錄:
接著安裝我們會用到的套件:
1 2 3 4 5 npm install @nestjs/typeorm typeorm reflect-metadata sqlite3 @nestjs/config class-validator class-transformer
2) 專案主要檔案與結構(我會逐一給出內容) 建議最終專案結構(重點):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 line-order-backend/ ├─ src/ │ ├─ main.ts │ ├─ app.module.ts │ ├─ config/ │ │ └─ configuration.ts │ ├─ database/ │ │ └─ database.module.ts │ └─ products/ │ ├─ products.module.ts │ ├─ products.controller.ts │ ├─ products.service.ts │ ├─ dto/ │ │ └─ create-product.dto.ts │ └─ entities/ │ └─ product.entity.ts ├─ .env ├─ .env.example ├─ package.json └─ tsconfig.json
下面是每個檔案的範本(請把檔案內容貼入相對路徑檔案):
src/main.ts 啟動並加入全域 ValidationPipe。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { ValidationPipe } from '@nestjs/common' ;import { NestFactory } from '@nestjs/core' ;import { AppModule } from './app.module' ;async function bootstrap ( ) { const app = await NestFactory .create (AppModule ); app.useGlobalPipes (new ValidationPipe ({ whitelist : true , transform : true , })); await app.listen (process.env .PORT ? +process.env .PORT : 3000 ); console .log (`🚀 Application running on: ${await app.getUrl()} ` ); } bootstrap ();
src/app.module.ts 主模組,載入 Config + Database + Products。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Module } from '@nestjs/common' ;import { ConfigModule } from '@nestjs/config' ;import configuration from './config/configuration' ;import { DatabaseModule } from './database/database.module' ;import { ProductsModule } from './products/products.module' ;@Module ({ imports : [ ConfigModule .forRoot ({ isGlobal : true , load : [configuration], }), DatabaseModule , ProductsModule , ], }) export class AppModule {}
src/config/configuration.ts 使用 @nestjs/config 讀 env。
1 2 3 4 5 6 7 8 9 10 11 export default () => ({ port : parseInt (process.env .PORT , 10 ) || 3000 , database : { type : process.env .DB_TYPE || 'sqlite' , host : process.env .DB_HOST || 'localhost' , port : parseInt (process.env .DB_PORT || '3306' , 10 ), username : process.env .DB_USER || 'root' , password : process.env .DB_PASSWORD || '' , database : process.env .DB_NAME || 'line_order_db' , }, });
src/database/database.module.ts TypeORM 連線模組(預設使用 SQLite,無需額外安裝 DB)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { Module } from '@nestjs/common' ;import { TypeOrmModule } from '@nestjs/typeorm' ;import configuration from '../config/configuration' ;import { ConfigModule , ConfigService } from '@nestjs/config' ;import { Product } from '../products/entities/product.entity' ;@Module ({ imports : [ ConfigModule , TypeOrmModule .forRootAsync ({ imports : [ConfigModule ], useFactory : async (configService : ConfigService ) => { const dbConfig = configService.get ('database' ); if (dbConfig.type === 'mysql' ) { return { type : 'mysql' as const , host : dbConfig.host , port : dbConfig.port , username : dbConfig.username , password : dbConfig.password , database : dbConfig.database , entities : [Product ], synchronize : true , }; } else { return { type : 'sqlite' as const , database : 'data/sqlite.db' , entities : [Product ], synchronize : true , }; } }, inject : [ConfigService ], }), ], }) export class DatabaseModule {}
注意:synchronize: true 僅用於開發;上線用 migration。
Products 模組(完整檔案) src/products/entities/product.entity.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { Entity , PrimaryGeneratedColumn , Column } from 'typeorm' ;@Entity ({ name : 'products' })export class Product { @PrimaryGeneratedColumn () id : number ; @Column ({ length : 150 }) name : string ; @Column ('decimal' , { precision : 10 , scale : 2 , default : 0 }) price : number ; @Column ({ default : 0 }) stock : number ; @Column ({ type : 'text' , nullable : true }) description?: string ; }
src/products/dto/create-product.dto.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { IsString , IsNotEmpty , IsNumber , IsOptional , Min } from 'class-validator' ;import { Type } from 'class-transformer' ;export class CreateProductDto { @IsString () @IsNotEmpty () name : string ; @Type (() => Number ) @IsNumber () @Min (0 ) price : number ; @Type (() => Number ) @IsNumber () @Min (0 ) stock : number ; @IsOptional () @IsString () description?: string ; }
src/products/products.service.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import { Injectable , NotFoundException } from '@nestjs/common' ;import { InjectRepository } from '@nestjs/typeorm' ;import { Repository } from 'typeorm' ;import { Product } from './entities/product.entity' ;import { CreateProductDto } from './dto/create-product.dto' ;@Injectable ()export class ProductsService { constructor ( @InjectRepository (Product) private readonly repo: Repository<Product>, ) {} async findAll (): Promise <Product []> { return this .repo .find ({ order : { id : 'DESC' } }); } async findOne (id : number ): Promise <Product > { const p = await this .repo .findOneBy ({ id }); if (!p) throw new NotFoundException ('Product not found' ); return p; } async create (dto : CreateProductDto ): Promise <Product > { const p = this .repo .create (dto as any ); return this .repo .save (p); } async remove (id : number ): Promise <void > { const r = await this .repo .delete (id); if (r.affected === 0 ) throw new NotFoundException ('Product not found' ); } }
src/products/products.controller.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { Controller , Get , Post , Body , Param , Delete , ParseIntPipe } from '@nestjs/common' ;import { ProductsService } from './products.service' ;import { CreateProductDto } from './dto/create-product.dto' ;@Controller ('products' )export class ProductsController { constructor (private readonly svc: ProductsService ) {} @Get () async findAll ( ) { return this .svc .findAll (); } @Get (':id' ) async findOne (@Param ('id' , ParseIntPipe) id: number ) { return this .svc .findOne (id); } @Post () async create (@Body () dto: CreateProductDto ) { return this .svc .create (dto); } @Delete (':id' ) async remove (@Param ('id' , ParseIntPipe) id: number ) { await this .svc .remove (id); return { message : 'deleted' }; } }
src/products/products.module.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 import { Module } from '@nestjs/common' ;import { TypeOrmModule } from '@nestjs/typeorm' ;import { ProductsService } from './products.service' ;import { ProductsController } from './products.controller' ;import { Product } from './entities/product.entity' ;@Module ({ imports : [TypeOrmModule .forFeature ([Product ])], providers : [ProductsService ], controllers : [ProductsController ], exports : [ProductsService ], }) export class ProductsModule {}
3) .env.example(放在專案根目錄) 建立 .env 或先複製 .env.example:
1 2 3 4 5 6 7 8 9 10 PORT =3000 DB_TYPE =sqliteDB_HOST =localhostDB_PORT =3306 DB_USER =rootDB_PASSWORD =DB_NAME =line_order_db
若要使用 MySQL(本機或 PlanetScale),把 DB_TYPE=mysql 並安裝 mysql2。
4) package.json scripts(若 nest new 已建立,確認 scripts 包含) package.json 中應有:
1 2 3 4 5 6 "scripts" : { "start" : "nest start" , "start:dev" : "nest start --watch" , "build" : "nest build" , "format" : "prettier --write \"src/**/*.ts\"" }
5) 執行與測試 啟動(dev):
1 2 3 4 5 mkdir -p datanpm run start:dev
測試 API(curl 或 Postman):
1 curl http://localhost:3000/products
1 2 3 curl -X POST http://localhost:3000/products \ -H "Content-Type: application/json" \ -d '{"name":"紅豆麵包","price":35,"stock":20,"description":"手作紅豆"}'
1 curl http://localhost:3000/products/1
6) 下一步建議(你要我帶的) 現在你已經有「第一塊積木」。你可以選接下來哪塊:
A. Orders Module (我會給你完整 OrdersModule 範例,含 DTO、Entity、Service 事務處理)
B. 把 Products 改成 MySQL(TypeORM + mysql2)並示範 migration
C. LineWebhook Module(整合 OrdersService)
D. 加入 Queue(Bull + Redis)並示範 webhook 非同步處理
你剛說要我「1 及 2(建立專案 + 告訴你每步該做什麼)」——已完成。 現在請選你想要我下一塊積木 (A/B/C/D)中的哪一項,我將馬上幫你生成完整程式碼與執行指令。