同一個需求:三種實作

需求:一個 User API,三個 endpoint:

  • POST /users:建立 user(name、email、age 必填,email 格式驗證)
  • GET /users/:id:取得單一 user
  • GET /users:列出所有 user(支援 ?page=1&limit=10

三個框架:FastAPI(Python)、NestJS(TypeScript)、Spring Boot(Java)。


FastAPI

# schemas.py
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
 
class UserCreate(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    email: EmailStr
    age: int = Field(ge=0, le=150)
 
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int
 
    class Config:
        from_attributes = True
 
# router.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
 
router = APIRouter(prefix="/users", tags=["users"])
 
@router.post("/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate, db: AsyncSession = Depends(get_db)):
    return await user_service.create(db, user)
 
@router.get("/{id}", response_model=UserResponse)
async def get_user(id: int, db: AsyncSession = Depends(get_db)):
    user = await user_service.find_by_id(db, id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user
 
@router.get("/", response_model=list[UserResponse])
async def list_users(
    page: int = Query(1, ge=1),
    limit: int = Query(10, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
):
    return await user_service.find_all(db, page=page, limit=limit)

FastAPI 的特徵

  • 程式碼量最少,signal-to-noise 最高
  • Schema 宣告(UserCreate)同時做型別定義 + 驗證規則
  • Depends(get_db) 是最輕量的 DI——一行就完成注入
  • OpenAPI 文件在 /docs 自動生成,零設定

NestJS

// create-user.dto.ts
import { IsEmail, IsInt, IsString, Length, Max, Min } from 'class-validator';
 
export class CreateUserDto {
  @IsString()
  @Length(1, 100)
  name: string;
 
  @IsEmail()
  email: string;
 
  @IsInt()
  @Min(0)
  @Max(150)
  age: number;
}
 
// user.entity.ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column({ length: 100 })
  name: string;
 
  @Column({ unique: true })
  email: string;
 
  @Column()
  age: number;
}
 
// users.module.ts
@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}
 
// users.controller.ts
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
 
  @Post()
  @HttpCode(201)
  create(@Body() dto: CreateUserDto) {
    return this.usersService.create(dto);
  }
 
  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findById(id);
  }
 
  @Get()
  findAll(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
  ) {
    return this.usersService.findAll({ page, limit });
  }
}

NestJS 的特徵

  • 程式碼量是 FastAPI 的 2 倍以上
  • 每個概念有自己的 class(DTO、Entity、Module、Controller、Service)
  • 強制架構——不會有人把邏輯塞在奇怪的地方,因為 Module 定義了邊界
  • ParseIntPipeDefaultValuePipe 讓 query param 型別安全,但要寫的字比 FastAPI 多

Spring Boot

// UserCreateDto.java
public record UserCreateDto(
    @NotBlank @Size(min = 1, max = 100) String name,
    @Email @NotBlank String email,
    @NotNull @Min(0) @Max(150) Integer age
) {}
 
// User.java (Entity)
@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(length = 100)
    private String name;
 
    @Column(unique = true)
    private String email;
 
    private Integer age;
 
    // Constructors, getters, setters...
}
 
// UserController.java
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
 
    private final UserService userService;
 
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@RequestBody @Valid UserCreateDto dto) {
        return userService.create(dto);
    }
 
    @GetMapping("/{id}")
    public UserDto findOne(@PathVariable Long id) {
        return userService.findById(id)
            .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }
 
    @GetMapping
    public Page<UserDto> findAll(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size
    ) {
        return userService.findAll(PageRequest.of(page, size));
    }
}

Spring Boot 的特徵

  • 程式碼量最多(Record / Entity / Controller 各自是獨立 class)
  • Java 的冗長但 annotation 讓意圖非常明確(@Valid@NotBlank@Email
  • Page<UserDto> 的 pagination 完全內建在 Spring Data JPA,不需要手寫
  • Lombok @RequiredArgsConstructor 節省了 constructor boilerplate

實際差距在哪

維度FastAPINestJSSpring Boot
同功能程式碼量最少中等(2x FastAPI)最多(3x FastAPI)
型別安全Pydantic,runtime 驗證class-validator + TypeScriptJava 靜態型別,最嚴格
架構強制性低(你決定結構)中高(Module 強制邊界)高(Spring Bean lifecycle)
DI 複雜度最低(Depends() 一行)中等(@Injectable() + Module)高(Bean scope / lifecycle)
Boilerplate最少中等最多
OpenAPI 文件自動,零設定需裝 @nestjs/swagger需裝 springdoc-openapi

樣板量多不代表壞

Spring Boot 的 boilerplate 最多——但這個「多」不全是負面的。

每個 class 有明確的責任(DTO、Entity、Controller、Service、Repository 各自分離),大型系統多人協作時不容易出現「邏輯寫在奇怪地方」的問題。FastAPI 雖然程式碼少,但在 50 人的系統裡,「要不要用 Pydantic model 做 service 層的資料傳遞」這個問題每個人答案不一樣,要花額外的 convention 討論。

樣板量是 trade-off,不是優劣判斷。選的時候問:「我的團隊規模和系統複雜度,需要多少強制架構?」


延伸閱讀