NestJS for Express Developers: A Practical Guide, With Prisma ORM

NestJS for Express Developers: A Practical Guide, With Prisma ORM

给 Express 开发者的 NestJS 实战指南(基于 Prisma ORM)

I’ve spent years building backends in Express. NestJS looks different on the surface, but here’s the thing nobody tells you clearly enough: if you already know Express, you don’t need to learn backend development again. You need to learn how NestJS organizes the things you already know how to do. 我用 Express 构建后端已经很多年了。NestJS 表面上看起来很不一样,但有一点没人跟你说清楚:如果你已经熟悉 Express,你根本不需要重新学习后端开发。你只需要学习 NestJS 是如何组织你已经掌握的那些技能的。

At first, NestJS looks like a lot more ceremony for the same result. A simple route handler turns into a controller class, a service class, decorators everywhere, and a module wiring them together. But once you understand why each piece exists, none of it feels like ceremony anymore; it feels like structure you were probably improvising by hand in every Express project anyway. 起初,NestJS 看起来为了实现同样的结果却需要繁琐的“仪式感”。一个简单的路由处理程序变成了控制器类、服务类、到处都是装饰器,还有一个将它们串联起来的模块。但一旦你理解了每个部分存在的原因,就不会再觉得这是繁琐的仪式了;这其实就是你在每个 Express 项目中可能都在手动构建的结构。

This post is the mental model I wish I’d had on day one, mapped directly against Express, using Prisma instead of TypeORM, since that’s the ORM I already use in production. 这篇文章是我希望在第一天就能拥有的思维模型,它直接映射了 Express 的概念,并使用 Prisma 而非 TypeORM,因为这是我在生产环境中实际使用的 ORM。

The one-sentence version: Think of NestJS as: Express, plus TypeScript, plus dependency injection, plus modules, plus decorators, plus enforced architecture. 一句话总结:把 NestJS 看作是:Express + TypeScript + 依赖注入 + 模块 + 装饰器 + 强制架构。

In Express, you might write: 在 Express 中,你可能会这样写:

app.post('/users', async (req, res) => {
  const user = await userService.create(req.body);
  res.json(user);
});

The same idea in NestJS: 在 NestJS 中实现同样的逻辑:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() body: CreateUserDto) {
    return this.usersService.create(body);
  }
}

More characters, same idea. The rest of this post is about why that extra structure exists and when it earns its keep. 代码变多了,但逻辑是一样的。本文接下来的部分将探讨为什么需要这些额外的结构,以及它们在何时能发挥价值。

The project structure

项目结构

Scaffold a new project, and you’ll get something like this: 初始化一个新项目,你会得到类似这样的结构:

nest-api/
├── src/
│ ├── app.controller.ts
│ ├── app.controller.spec.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test/
├── package.json
├── tsconfig.json
└── nest-cli.json

Four files matter to start: main.ts, app.module.ts, app.controller.ts, app.service.ts. Everything else is scaffolding you’ll grow into. 起步时,有四个文件至关重要:main.tsapp.module.tsapp.controller.tsapp.service.ts。其余的都是随着项目成长才会用到的脚手架。

main.ts is your entry point: main.ts 是你的入口文件:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

If you’re coming from Express, this is your const app = express(); app.listen(3000);, just bootstrapped from a root module instead of a bare instance. NestFactory.create(AppModule) is Nest reading your entire application’s shape from one root module and building it from there. 如果你来自 Express,这等同于你的 const app = express(); app.listen(3000);,只不过它是从一个根模块启动,而不是直接实例化。NestFactory.create(AppModule) 的作用是让 Nest 从一个根模块读取整个应用程序的形态,并从那里开始构建。

app.module.ts is the first genuinely unfamiliar concept: app.module.ts 是第一个真正陌生的概念:

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Hold off on fully understanding modules yet; they’ll make more sense once you’ve seen a controller and a service in place. 先别急着完全理解模块;等你见过控制器和服务是如何运作的,它们就会变得好理解多了。

The architecture

架构

HTTP Request


┌─────────────┐
│   Module    │
└──────┬──────┘


┌─────────────┐
│ Controller  │
└──────┬──────┘


┌─────────────┐
│   Service   │
└──────┬──────┘


┌─────────────┐
│   Prisma    │
└──────┬──────┘


┌─────────────┐
│ PostgreSQL  │
└─────────────┘

The pieces worth knowing, roughly in the order you’ll actually meet them: 值得了解的组件,大致按照你实际接触它们的顺序排列:

  • Module: organizes a feature (模块:组织功能)
  • Controller: handles HTTP requests (控制器:处理 HTTP 请求)
  • Service: contains business logic (服务:包含业务逻辑)
  • DTO: describes and validates incoming data (DTO:描述并验证传入数据)
  • Guard: controls authentication and authorization (守卫:控制身份验证和授权)
  • Pipe: validates or transforms input (管道:验证或转换输入)
  • Interceptor: wraps or transforms request and response behavior (拦截器:包装或转换请求和响应行为)
  • Middleware: the same concept as Express middleware (中间件:与 Express 中间件概念相同)
  • Dependency injection: Nest supplies each class the dependencies it needs, instead of you constructing them by hand (依赖注入:Nest 为每个类提供所需的依赖,而不是由你手动构建)

Don’t try to hold all of these at once. Controllers and services get you most of the way there. 不要试图一次性记住所有这些。掌握控制器和服务就能解决大部分问题。

Your Express knowledge maps directly

你的 Express 知识可以直接映射

This is genuinely the fastest way to internalize it. 这确实是内化这些概念最快的方法。

ExpressNestJS
app.get()@Get()
app.post()@Post()
req.params@Param()
req.body@Body()
req.query@Query()
res.json()return
MiddlewareMiddleware
RouterController
ServiceService
modulesModules
Providers and servicesProviders and services
Manual dependency passingDependency injection
Validation middlewarePipes
Auth middlewareGuards
Error middlewareException filters
Router organizationModules

Router organization 路由组织

Express:

router.get('/users/:id', async (req, res) => {
  const user = await usersService.findById(req.params.id);
  res.json(user);
});

NestJS:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get(':id')
  findById(@Param('id') id: string) {
    return this.usersService.findById(id);
  }
}

The NestJS version is saying, out loud, in the type system: this class handles /users, and this method handles GET /users/:id. That’s the whole trade the extra syntax is making: implicit routing conventions become explicit, checkable structure. NestJS 版本通过类型系统明确地表达了:这个类处理 /users,而这个方法处理 GET /users/:id。这就是额外语法所带来的全部交换:隐式的路由约定变成了显式的、可检查的结构。

Controllers 控制器

This is where you’ll feel most at home immediately. 这是你会感到最亲切的地方。

@Controller('users')
export class UsersController {
  @Get()
  findAll() { return []; }

  @Get(':id')
  findOne(@Param('id') id: string) { return { id }; }

  @Post()
  create(@Body() body: any) { return body; }

  @Delete(':id')
  remove(@Param('id') id: string) { return { id }; }
}

That’s the equivalent of router.get('/users', ...), router.get('/users/:id', ...), router.post('/users', ...), and router.delete('/users/:id', ...). Nothing conceptually new here. 这等同于 router.get('/users', ...)router.get('/users/:id', ...)router.post('/users', ...)router.delete('/users/:id', ...)。在概念上没有任何新东西。

Services, where Nest starts diverging 服务:Nest 开始分道扬镳的地方

In Express you might write: 在 Express 中你可能会写:

const userService = {
  async findAll() { return db.user.findMany(); },
  async findOne(id: string) { return db.user.findUnique({ where: { id } }); },
};

In NestJS: 在 NestJS 中:

@Injectable()
export class UsersService {
  async findAll() { return []; }
  async findOne(id: string) { return { id }; }
}

@Injectable() is the important part. It tells Nest that this class can be managed by Nest’s dependency injection system. Your controller then does this: @Injectable() 是关键部分。它告诉 Nest 这个类可以由 Nest 的依赖注入系统管理。然后你的控制器会这样做:

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
}

Notice what’s missing: const usersService = new UsersService() never appears anywhere. Nest creates it and hands it to the controller. That’s dependency injection, and it’s worth its own section, because it’s the concept… 注意少了什么:const usersService = new UsersService() 从未出现过。Nest 会创建它并将其交给控制器。这就是依赖注入,它值得单独开辟一个章节,因为它是这个概念的核心……