跳转至

第 6 章:Node.js 后端开发

学习时间:5 小时 | 难度:⭐⭐⭐ 中级 | 前置知识:第 1-5 章,HTTP 基础


本章概览

本章使用 Fastify + TypeScript + Prisma 构建生产级 REST API,这是 2025 年 Node.js 后端最主流的技术栈。

学习目标:

  • 掌握 Fastify 的类型安全路由和插件系统
  • 使用 Zod 进行运行时数据验证与类型推断
  • 掌握 Prisma ORM 的 TypeScript 集成
  • 理解 JWT 认证与中间件设计
  • 构建完整的 CRUD REST API

6.1 Fastify + TypeScript 项目搭建

Bash
mkdir api-server && cd api-server
pnpm init
pnpm add fastify @fastify/jwt @fastify/cors @fastify/helmet
pnpm add zod @fastify/type-provider-zod  # 类型安全的请求验证
pnpm add @prisma/client                   # 数据库 ORM
pnpm add -D typescript tsx @types/node prisma
npx prisma init
TypeScript
// src/app.ts — Fastify 应用主文件
import Fastify from "fastify";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "@fastify/type-provider-zod";
import cors from "@fastify/cors";
import helmet from "@fastify/helmet";
import jwt from "@fastify/jwt";

export function buildApp() {
  const app = Fastify({
    logger: {
      level: process.env.NODE_ENV === "production" ? "info" : "debug",
      // 生产环境用 pino-pretty 格式化日志
    },
  }).withTypeProvider<ZodTypeProvider>(); // 开启 Zod 类型提供器

  // 注册编译器(Zod 作为验证器)
  app.setValidatorCompiler(validatorCompiler);
  app.setSerializerCompiler(serializerCompiler);

  // 插件:安全头、CORS、JWT
  app.register(helmet);
  app.register(cors, {
    origin: process.env.ALLOWED_ORIGINS?.split(",") ?? ["http://localhost:3000"],
    credentials: true,
  });
  app.register(jwt, {
    secret: process.env.JWT_SECRET!,
    sign: { expiresIn: "7d" },
  });

  // 注册路由(模块化)
  app.register(import("./routes/users"), { prefix: "/api/users" });
  app.register(import("./routes/auth"),  { prefix: "/api/auth" });
  app.register(import("./routes/posts"), { prefix: "/api/posts" });

  // 健康检查
  app.get("/health", () => ({ status: "ok", timestamp: new Date().toISOString() }));

  return app;
}

// src/server.ts — 启动入口
import { buildApp } from "./app";

const app = buildApp();

app.listen({ port: Number(process.env.PORT ?? 3000), host: "0.0.0.0" }, (err) => {
  if (err) {
    app.log.error(err);
    process.exit(1);
  }
});

6.2 Zod 运行时验证 + 类型推断

Zod 是 TypeScript 优先的数据校验库——一份 Schema,同时获得运行时验证和 TypeScript 类型

TypeScript
// src/schemas/user.schema.ts
import { z } from "zod";

// 定义 Schema(运行时验证规则)
export const CreateUserSchema = z.object({
  name:     z.string().min(2).max(50),
  email:    z.string().email("Invalid email format"),
  password: z.string().min(8).regex(/(?=.*[A-Z])(?=.*[0-9])/, "Must include uppercase and digit"),
  role:     z.enum(["admin", "user"]).default("user"),
});

export const UpdateUserSchema = CreateUserSchema.partial().omit({ password: true });

export const UserIdSchema = z.object({
  id: z.string().uuid("Invalid user ID format"),
});

export const QueryUsersSchema = z.object({
  page:    z.coerce.number().int().positive().default(1),
  limit:   z.coerce.number().int().positive().max(100).default(20),
  search:  z.string().optional(),
  role:    z.enum(["admin", "user"]).optional(),
});

// 从 Schema 提取 TypeScript 类型(无需重复定义!)
export type CreateUserDto  = z.infer<typeof CreateUserSchema>;
export type UpdateUserDto  = z.infer<typeof UpdateUserSchema>;
export type QueryUsersDto  = z.infer<typeof QueryUsersSchema>;

6.3 类型安全的路由

TypeScript
// src/routes/users.ts
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod";
import { z } from "zod";
import { CreateUserSchema, UpdateUserSchema, UserIdSchema, QueryUsersSchema } from "../schemas/user.schema";
import { UserService } from "../services/user.service";

const userService = new UserService();

const usersRoutes: FastifyPluginAsyncZod = async (app) => {
  // GET /api/users — 查询用户列表
  app.get("/", {
    schema: {
      querystring: QueryUsersSchema,
      response: {
        200: z.object({
          users: z.array(z.object({
            id:    z.string(),
            name:  z.string(),
            email: z.string(),
            role:  z.enum(["admin", "user"]),
          })),
          total: z.number(),
          page:  z.number(),
        }),
      },
      tags: ["users"],
      summary: "List users with pagination",
    },
  }, async (request) => {
    // request.query 类型完全推断自 QueryUsersSchema!
    const { page, limit, search, role } = request.query;
    return userService.findAll({ page, limit, search, role });
  });

  // GET /api/users/:id — 获取单个用户
  app.get("/:id", {
    schema: {
      params: UserIdSchema,
    },
  }, async (request, reply) => {
    const user = await userService.findById(request.params.id);
    if (!user) {
      return reply.status(404).send({ error: "User not found" });
    }
    return user;
  });

  // POST /api/users — 创建用户(需要 admin 权限)
  app.post("/", {
    onRequest: [app.authenticate, app.requireAdmin], // 认证中间件
    schema: {
      body: CreateUserSchema,
      response: { 201: z.object({ id: z.string(), message: z.string() }) },
    },
  }, async (request, reply) => {
    // request.body 类型完全推断:CreateUserDto
    const newUser = await userService.create(request.body);
    return reply.status(201).send({ id: newUser.id, message: "User created" });
  });

  // PATCH /api/users/:id — 更新用户
  app.patch("/:id", {
    onRequest: [app.authenticate],
    schema: {
      params: UserIdSchema,
      body: UpdateUserSchema,
    },
  }, async (request, reply) => {
    const updated = await userService.update(request.params.id, request.body);
    if (!updated) return reply.status(404).send({ error: "User not found" });
    return updated;
  });

  // DELETE /api/users/:id
  app.delete("/:id", {
    onRequest: [app.authenticate, app.requireAdmin],
    schema: { params: UserIdSchema },
  }, async (request, reply) => {
    await userService.delete(request.params.id);
    return reply.status(204).send();
  });
};

export default usersRoutes;

6.4 Prisma ORM 集成

Text Only
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(uuid())
  name      String
  email     String   @unique
  password  String
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Post {
  id        String   @id @default(uuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum Role {
  USER
  ADMIN
}
TypeScript
// src/lib/prisma.ts — 单例 Prisma 客户端
import { PrismaClient } from "@prisma/client";

// 防止开发环境热重载时创建多个连接
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"],
});

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

// src/services/user.service.ts
import { prisma } from "../lib/prisma";
import { type CreateUserDto, type UpdateUserDto, type QueryUsersDto } from "../schemas/user.schema";
import bcrypt from "bcryptjs";

export class UserService {
  async findAll({ page, limit, search, role }: QueryUsersDto) {
    const where = {
      ...(search ? {
        OR: [
          { name:  { contains: search, mode: "insensitive" as const } },
          { email: { contains: search, mode: "insensitive" as const } },
        ],
      } : {}),
      ...(role ? { role: role.toUpperCase() as "ADMIN" | "USER" } : {}),
    };

    const [users, total] = await Promise.all([
      prisma.user.findMany({
        where,
        skip: (page - 1) * limit,
        take: limit,
        select: { id: true, name: true, email: true, role: true, createdAt: true },
        orderBy: { createdAt: "desc" },
      }),
      prisma.user.count({ where }),
    ]);

    return { users, total, page };
  }

  async findById(id: string) {
    return prisma.user.findUnique({
      where: { id },
      select: { id: true, name: true, email: true, role: true, createdAt: true },
    });
  }

  async create(data: CreateUserDto) {
    const hashedPassword = await bcrypt.hash(data.password, 12);
    return prisma.user.create({
      data: {
        ...data,
        password: hashedPassword,
        role: data.role.toUpperCase() as "ADMIN" | "USER",
      },
    });
  }

  async update(id: string, data: UpdateUserDto) {
    return prisma.user.update({
      where: { id },
      data,
    }).catch(() => null); // 如果不存在返回 null
  }

  async delete(id: string) {
    return prisma.user.delete({ where: { id } });
  }
}

6.5 JWT 认证中间件

TypeScript
// src/plugins/auth.ts
import type { FastifyPluginAsync, FastifyRequest, FastifyReply } from "fastify";
import fp from "fastify-plugin";

// 扩展 Fastify 的类型声明
declare module "fastify" {
  interface FastifyInstance {
    authenticate:  (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
    requireAdmin:  (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
  }
  interface FastifyRequest {
    user: { id: string; email: string; role: "ADMIN" | "USER" };
  }
}

const authPlugin: FastifyPluginAsync = async (app) => {
  // 验证 JWT
  app.decorate("authenticate", async (request: FastifyRequest, reply: FastifyReply) => {
    try {
      await request.jwtVerify();
    } catch {
      reply.status(401).send({ error: "Unauthorized" });
    }
  });

  // 要求管理员角色
  app.decorate("requireAdmin", async (request: FastifyRequest, reply: FastifyReply) => {
    if (request.user.role !== "ADMIN") {
      reply.status(403).send({ error: "Forbidden: Admin role required" });
    }
  });
};

export default fp(authPlugin);

📌 本章小结

技术 版本 作用
Fastify 5.x 高性能 HTTP 框架(比 Express 快 2-3 倍)
Zod 3.x 运行时验证 + 类型自动推断
Prisma 6.x Type-safe ORM,自动生成类型
@fastify/type-provider-zod Fastify 与 Zod 集成(Schema 自动类型推断)

下一章:Next.js 15 全栈开发 + tRPC v11 端到端类型安全。


Node.js 22 · Fastify 5 · Prisma 6 · 2025