同一個需求:三種實作
需求:一個 User API,三個 endpoint:
POST /users:建立 user(name、email、age 必填,email 格式驗證)GET /users/:id:取得單一 userGET /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 定義了邊界
ParseIntPipe、DefaultValuePipe讓 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
實際差距在哪
| 維度 | FastAPI | NestJS | Spring Boot |
|---|---|---|---|
| 同功能程式碼量 | 最少 | 中等(2x FastAPI) | 最多(3x FastAPI) |
| 型別安全 | Pydantic,runtime 驗證 | class-validator + TypeScript | Java 靜態型別,最嚴格 |
| 架構強制性 | 低(你決定結構) | 中高(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,不是優劣判斷。選的時候問:「我的團隊規模和系統複雜度,需要多少強制架構?」
