2025-07-26 Line 訂單系統- 先做「NestJS 第一個練習(Products 模組)」

2025-07-26 Line 訂單系統- 先做「NestJS 第一個練習(Products 模組)」

先做「NestJS 第一個練習(Products 模組)」

我會一步步帶你從 0 建出 可運行、可擴充的 NestJS 專案骨架,並把 Products Module(含 Controller/Service/DTO/Entity/DI) 實作好,讓你能像「堆積木」一樣繼續往 Orders、Webhook、Redis 等積木擴充。

下面的流程分兩部分:

  1. 建立專案骨架(用 Nest CLI)
  2. 把 Products Module 的所有檔案都寫好(可直接執行)

我會給你完整命令與每個檔案的程式碼——你只要複製貼上即可啟動。


0) 前置:環境需求

確保你系統有 Node.js(>=16)、npm/yarn。
建議用 npm


1) 建立 NestJS 專案(骨架)

在終端機執行:

1
2
3
4
5
6
# 全域安裝 Nest CLI(若尚未安裝)
npm install -g @nestjs/cli

# 建新專案(選 TypeScript)
nest new line-order-backend
# 選 npm 作為 package manager(或 yarn)

進入專案目錄:

1
cd line-order-backend

接著安裝我們會用到的套件:

1
2
3
4
5
# 核心 + typeorm + validation + sqlite (簡單起步)
npm install @nestjs/typeorm typeorm reflect-metadata sqlite3 @nestjs/config class-validator class-transformer

# 若你以後要用 MySQL,還要安裝 mysql2:
# npm install mysql2

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', // sqlite or mysql
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, // dev only; use migrations in prod
};
} else {
// default sqlite for quick local dev
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
# .env.example
PORT=3000

# DB: use sqlite by default. To use mysql change DB_TYPE to 'mysql' and fill DB_* values
DB_TYPE=sqlite
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_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
# 建議先建立 SQLite 資料夾
mkdir -p data

# 啟動開發模式
npm 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)中的哪一項,我將馬上幫你生成完整程式碼與執行指令。