在 Monorepo 中使用 pnpm 设置 Fastify
Source: Dev.to
在 Monorepo 中使用 pnpm 搭建 Fastify 项目
在这篇文章中,我将向大家展示如何在一个使用 pnpm 管理的 monorepo 中快速搭建 Fastify 应用。我们会一步步完成以下内容:
- 初始化 monorepo 并配置 pnpm 工作区
- 创建共享的 TypeScript 配置
- 在子包中添加 Fastify 服务器
- 使用
pnpm run dev同时启动多个服务
前置条件
在开始之前,请确保你的机器上已经安装了以下工具:
- Node.js(推荐 v18+)
- pnpm(
npm i -g pnpm) - Git(可选,用于版本控制)
如果你还没有安装 pnpm,可以运行:
npm i -g pnpm
第一步:初始化 monorepo
在一个空目录下运行:
pnpm init
随后在根目录的 package.json 中添加 workspaces 配置:
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
]
}
这会告诉 pnpm 所有位于 packages 文件夹下的子项目都是工作区的一部分。
第二步:创建共享的 TypeScript 配置
在根目录新建 tsconfig.base.json,内容如下:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"*": ["node_modules/*"]
}
}
}
每个子包的 tsconfig.json 只需要继承这个基础配置:
{
"extends": "../../tsconfig.base.json",
"include": ["src"]
}
第三步:在 packages 里创建 Fastify 服务
mkdir -p packages/api/src
cd packages/api
pnpm init -y
pnpm add fastify
pnpm add -D typescript ts-node @types/node
在 packages/api/src/index.ts 中写入最简的 Fastify 示例:
import Fastify from 'fastify';
const server = Fastify({ logger: true });
server.get('/', async (request, reply) => {
return { hello: 'world' };
});
const start = async () => {
try {
await server.listen({ port: 3000 });
console.log('🚀 Server listening on http://localhost:3000');
} catch (err) {
server.log.error(err);
process.exit(1);
}
};
start();
第四步:在根目录添加开发脚本
在根 package.json 中加入以下脚本,以便一次性启动所有子服务:
{
"scripts": {
"dev": "pnpm -r run dev"
}
}
然后在 packages/api/package.json 中添加对应的 dev 脚本:
{
"scripts": {
"dev": "ts-node src/index.ts"
}
}
现在运行:
pnpm dev
pnpm 会递归地在每个工作区执行 dev 脚本,Fastify 服务器将会在 3000 端口启动。
进阶:使用 nodemon 热重载
如果想在代码变更时自动重启服务器,可以在 api 包里安装 nodemon:
pnpm add -D nodemon
并把 dev 脚本改为:
{
"scripts": {
"dev": "nodemon --watch src --exec ts-node src/index.ts"
}
}
小结
- pnpm workspaces 能够让我们在 monorepo 中共享依赖,避免重复安装。
- 通过 共享的 tsconfig,所有子包都能保持一致的 TypeScript 编译选项。
- 使用根目录的
pnpm dev脚本,可以一次性启动多个微服务,提升开发效率。
希望这篇指南能帮助你快速在 monorepo 环境下使用 Fastify。如果你有任何问题或改进建议,欢迎在评论区留言!
前置条件
- Node.js(v22 或更高)
- 已安装 pnpm
Monorepo 结构
最终的结构如下所示:
app-monorepo/
├── package.json
├── pnpm-workspace.yaml
├── apps/
│ └── api/
│ ├── package.json
│ ├── tsconfig.json
│ └── src/
│ ├── app.ts
│ ├── server.ts
│ ├── routes/
│ │ ├── root.ts
│ │ └── users/
│ │ └── index.ts
│ └── plugins/
│ ├── cors.ts
│ ├── helmet.ts
│ └── sensible.ts
└── tsconfig.json
注意: 将
app.ts与server.ts分离是 Fastify 官方约定。它允许您在不启动 HTTP 服务器的情况下测试应用程序。
初始化 Monorepo
mkdir app-monorepo
cd app-monorepo
pnpm init
配置 pnpm 工作区
在根目录创建 pnpm-workspace.yaml 文件:
packages:
- 'apps/*'
配置 TypeScript(根目录)
在根目录创建 tsconfig.json 文件:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true
},
"exclude": ["**/node_modules", "**/dist"]
}
创建 API 包
mkdir -p apps/api/src/{routes/users,plugins}
cd apps/api
pnpm init
apps/api/package.json:
{
"name": "@monorepo/api",
"version": "1.0.0",
"type": "commonjs",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"test": "node --test",
"type-check": "tsc --noEmit"
},
"dependencies": {
"fastify": "^5.7.4",
"@fastify/autoload": "^6.3.1",
"@fastify/cors": "^11.2.0",
"@fastify/helmet": "^13.0.2",
"@fastify/sensible": "^6.0.4",
"close-with-grace": "^2.4.0",
"fastify-plugin": "^5.1.0"
},
"devDependencies": {
"@types/node": "^25.2.3",
"pino-pretty": "^13.1.3",
"tsx": "^4.21.0",
"typescript": "~5.9.3"
}
}
现在安装依赖:
pnpm install
配置 API 的 TypeScript
apps/api/tsconfig.json:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"tsBuildInfoFile": "./dist/.tsbuildinfo",
"composite": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
创建应用工厂
官方 Fastify 约定是将 app.ts(注册插件和路由的工厂)与 server.ts(启动服务器的入口点)分离。
apps/api/src/app.ts
import path from 'path';
import AutoLoad from '@fastify/autoload';
import { FastifyInstance, FastifyPluginOptions, FastifyServerOptions } from 'fastify';
// Fastify server options
export const options: FastifyServerOptions = {
logger: {
level: process.env.LOG_LEVEL || 'debug',
transport:
process.env.LOG_LEVEL === 'debug'
? {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
colorize: true
}
}
: undefined
}
};
export default async function app(
fastify: FastifyInstance,
opts: FastifyPluginOptions
) {
// Automatically load all plugins from the plugins/ folder
await fastify.register(AutoLoad, {
dir: path.join(__dirname, 'plugins'),
options: { ...opts }
});
// Automatically load all routes from the routes/ folder
await fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: { ...opts }
});
}
Source: …
创建服务器入口点
close-with-grace 是 Fastify 团队推荐的优雅关闭(graceful shutdown)包。它在一个地方统一处理 SIGINT、SIGTERM 和 uncaughtException,并且可配置的 delay 能让正在进行的请求有时间完成后再关闭服务器。
先安装它:
pnpm --filter @monorepo/api add close-with-grace
apps/api/src/server.ts
import Fastify from 'fastify';
import closeWithGrace from 'close-with-grace';
import app, { options } from './app';
// 加载 .env 文件(可选)
try {
process.loadEnvFile();
} catch {
// .env 是可选的
}
// 使用 app.ts 导出的 options 实例化 Fastify(已包含 logger)
const server = Fastify(options);
// 将 app 注册为插件
server.register(app);
// 优雅关闭配置
const closeListeners = closeWithGrace(
{ delay: parseInt(process.env.FASTIFY_CLOSE_GRACE_DELAY ?? '500') || 500 },
async function ({ err }) {
if (err) {
server.log.error(err);
}
await server.close();
}
);
server.addHook('onClose', async () => {
closeListeners.uninstall();
});
// 开始监听
const PORT = parseInt(process.env.FASTIFY_PORT ?? '3000') || 3000;
server.listen({ port: PORT, host: '0.0.0.0' }, (err) => {
if (err) {
server.log.error({ err }, 'Server shutdown due to an error');
process.exit(1);
}
});
现在你已经在 pnpm 工作区 monorepo 中拥有一个完整配置的 Fastify 项目。使用 pnpm --filter @monorepo/api dev 启动开发服务器,pnpm --filter @monorepo/api build 编译,pnpm --filter @monorepo/api start 运行构建产物。祝编码愉快!
自动加载所有路由
await fastify.register(AutoLoad, {
dir: path.join(__dirname, 'routes'),
options: { ...opts }
});
注意:
pino‑pretty仅在非生产环境下启用。在生产环境中,原始 JSON 日志更高效,并且更好地被日志聚合器支持。
创建插件
插件遵循使用 fastify-plugin 将装饰暴露到全局上下文的约定。
apps/api/src/plugins/cors.ts
import fp from 'fastify-plugin';
import { FastifyInstance } from 'fastify';
import Cors from '@fastify/cors';
async function corsPlugin(fastify: FastifyInstance) {
await fastify.register(Cors, {
origin: true,
methods: ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'],
credentials: true
});
}
export default fp(corsPlugin, {
name: 'cors'
});
apps/api/src/plugins/helmet.ts
import fp from 'fastify-plugin';
import { FastifyInstance } from 'fastify';
import helmet from '@fastify/helmet';
async function helmetPlugin(fastify: FastifyInstance) {
await fastify.register(helmet, {
crossOriginResourcePolicy: { policy: 'same-origin' },
crossOriginEmbedderPolicy: true
// 其他 Helmet 选项
});
}
export default fp(helmetPlugin, {
name: 'helmet'
});
apps/api/src/plugins/sensible.ts
import fp from 'fastify-plugin';
import sensible from '@fastify/sensible';
export default fp(async (fastify) => {
await fastify.register(sensible);
});
为什么使用
fastify-plugin?
如果不使用它,每个插件都有自己的封装作用域。使用fastify-plugin后,装饰和钩子会暴露给父级上下文,从而在全局可用。
创建路由
routes/ 文件夹中的每个文件都会被 @fastify/autoload 自动加载。文件夹结构对应路由路径。
apps/api/src/routes/root.ts
import { FastifyInstance } from 'fastify';
export default async function (fastify: FastifyInstance) {
fastify.get('/', async (request, reply) => {
return { message: 'Hello from Fastify in monorepo!' };
});
fastify.get('/health', async (request, reply) => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
}
apps/api/src/routes/users/index.ts
import { FastifyInstance } from 'fastify';
export default async function (fastify: FastifyInstance) {
fastify.get('/', async (request, reply) => {
return [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' }
];
});
fastify.get('/:id', async (request, reply) => {
const { id } = request.params as { id: string };
if (isNaN(parseInt(id))) {
return reply.badRequest('ID must be a number');
}
return { id: parseInt(id), name: 'User ' + id };
});
}
注意:
@fastify/autoload会自动将routes/users/index.ts映射到/users路径。无需在app.ts中手动注册路由。
根脚本
package.json 位于仓库根目录:
{
"scripts": {
"dev": "pnpm --filter @monorepo/api dev",
"build": "pnpm -r build",
"start": "pnpm --filter @monorepo/api start",
"test": "pnpm --filter @monorepo/api test",
"type-check": "pnpm -r type-check"
}
}
有用命令示例
# Development
pnpm dev
# Build all packages
pnpm build
# Production
pnpm start
# Test
pnpm test
# Add a dependency to the API
pnpm --filter @monorepo/api add fastify-plugin
# Add a dev dependency
pnpm --filter @monorepo/api add -D @types/node
希望本指南能帮助您搭建 Fastify monorepo。随着改进的进行,本帖将持续更新。