在实际的 Web 应用开发中,我们经常需要在同一个服务器上同时提供 API 接口和静态文件服务。例如:
NestJS 的 @nestjs/serve-static 包正是为了解决这些需求而设计的。它可以让你的 NestJS 应用同时充当 API 服务器和静态文件服务器。
你有一个 React 前端项目和 NestJS 后端项目,希望部署在同一个服务器上:
通过 ServeStaticModule,你可以让 NestJS 应用:
/api/* 路径的 API 请求。你开发了一个文件管理系统,用户可以上传文件,然后通过 URL 直接访问:
上传的文件存储在:/uploads/documents/report.pdf
用户可通过访问:http://yourapp.com/files/report.pdf首先,安装所需的依赖包:
npm install @nestjs/serve-static最常见的用法是托管前端单页应用。在根模块中配置:
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { ServeStaticModule } from '@nestjs/serve-static'
import { join } from 'node:path'
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client'), // 指向前端构建产物目录
exclude: ['/api/(.*)'], // 排除 API 路由,避免冲突
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}这样配置后:
http://localhost:3000/ 会返回 client/index.html。http://localhost:3000/about 也会返回 client/index.html(支持前端路由)。http://localhost:3000/api/users 会正常调用你的 API 控制器。如果你需要托管多个不同用途的静态文件目录:
@Module({
imports: [
ServeStaticModule.forRoot(
{
rootPath: join(__dirname, '..', 'public'), // 公共资源
serveRoot: '/assets', // 通过 /assets/* 访问
},
{
rootPath: join(__dirname, '..', 'uploads'), // 用户上传文件
serveRoot: '/files', // 通过 /files/* 访问
}
),
],
// ...
})
export class AppModule {}这样你就可以:
/assets/logo.png 访问 public/logo.png。/files/document.pdf 访问 uploads/document.pdf。ServeStaticModule 提供了丰富的配置选项来满足不同需求:
为静态文件设置合适的缓存策略:
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client'),
serveStaticOptions: {
cacheControl: 'public, max-age=31536000', // 缓存一年
},
})当文件不存在时的处理方式:
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client'),
renderPath: '/', // 所有路径都返回根目录的 index.html
serveStaticOptions: {
fallthrough: false, // 不存在的文件直接返回 404
},
})如果你使用的是 Fastify 而不是 Express:
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'client'),
serveStaticOptions: {
fallthrough: true, // Fastify 需要启用此选项以支持 SPA 路由
},
})当你同时托管前端应用和提供 API 服务时,需要明确区分哪些路径由静态文件处理,哪些由 API 控制器处理。exclude: ['/api/(.*)'] 确保所有以 /api/ 开头的请求都交给你的控制器处理,而不是尝试寻找对应的静态文件。
默认情况下,模块会将所有未匹配到静态文件的请求都指向 index.html,这样前端路由(如 React Router)就能正常工作。当用户直接访问 http://yourapp.com/products/123 时,即使服务器上没有对应的文件,也会返回 index.html,然后由前端 JavaScript 处理路由跳转。
如果你使用 Fastify 适配器,务必将 fallthrough 设置为 true,否则 SPA 路由将无法正常工作。
假设你开发了一个博客系统,目录结构如下:
配置示例:
ServeStaticModule.forRoot(
{
rootPath: join(__dirname, '..', 'blog-frontend'),
exclude: ['/api/(.*)'], // API 路由
},
{
rootPath: join(__dirname, '..', 'admin-panel'),
serveRoot: '/admin', // 管理后台通过 /admin 访问
},
{
rootPath: join(__dirname, '..', 'uploads'),
serveRoot: '/images', // 图片通过 /images 访问
}
)企业官网需要同时提供静态页面和 API 接口:
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'website-dist'),
exclude: ['/api/(.*)'], // API 接口
serveStaticOptions: {
cacheControl: 'public, max-age=86400', // 缓存 24 小时
},
})现在访问:
https://company.com/ → 官网首页。https://company.com/about → 关于我们页面。https://company.com/api/contact → 联系表单 API。完整的配置选项说明请参见 serve-static-options.interface.ts。
你也可以查看官方提供的完整示例来了解更多用法。