在 Monorepo 中使用 pnpm 设置 Fastify

发布: (2026年2月19日 GMT+8 05:37)
10 分钟阅读
原文: Dev.to

Source: Dev.to

在 Monorepo 中使用 pnpm 搭建 Fastify 项目

在这篇文章中,我将向大家展示如何在一个使用 pnpm 管理的 monorepo 中快速搭建 Fastify 应用。我们会一步步完成以下内容:

  • 初始化 monorepo 并配置 pnpm 工作区
  • 创建共享的 TypeScript 配置
  • 在子包中添加 Fastify 服务器
  • 使用 pnpm run dev 同时启动多个服务

前置条件

在开始之前,请确保你的机器上已经安装了以下工具:

  • Node.js(推荐 v18+)
  • pnpmnpm 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.tsserver.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)包。它在一个地方统一处理 SIGINTSIGTERMuncaughtException,并且可配置的 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。随着改进的进行,本帖将持续更新。

0 浏览
Back to Blog

相关文章

阅读更多 »

伪装的拖延

介绍 最近我开始探索 TypeScript。获得新技能的最初兴奋很快被压倒性和困惑的情绪所取代——尤其是...