npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pylonts/fastify

v1.1.8

Published

A thin wrapper on top of Fastify — decorator-based routing, JWT auth, HMAC signature, JSON Schema validation, DB connection pooling, auto-scanned controllers.

Readme

fastify-api

Fastify 之上的一层薄封装。开发者只需写 controller / service / dao,路由注册、token 鉴权、JSON Schema 校验、统一异常处理全部自动处理。token 还原(Redis)由独立包提供(@pylonts/token-redis-driver),框架本身零持久化依赖。

核心理念:极简。不包装 Happy Path,不做 { code, data, message } 统一响应格式。Controller 返回什么就是什么,异常由框架统一捕获。

安装

npm install @pylonts/fastify

快速开始

1. 入口文件

// src/main.ts
import { createApp } from '@pylonts/fastify';
import config from './config';

const app = await createApp(config);
await app.listen({ port: 3000 });
// src/config.ts
import type { AppConfig } from '@pylonts/fastify';

const config: AppConfig = {
  auth: {
    token: {
      // 还原 token 平面对象:Redis key = `${appName}.${token}`
      // 用 @pylonts/token-redis-driver 的 getToken 接入
      resolveToken: async (appName: string, token: string) => {
        throw new Error('resolveToken not wired — configure Redis-backed token restore');
      },
      // app 级引导密钥(登录入口无 token 时验签,初始密钥方案 2)
      bootstrapSecrets: {
        admin: process.env.BOOTSTRAP_SECRET_ADMIN || 'dev-bootstrap-change-me',
      },
      // refreshToken 派生密钥盐
      refreshKeySalt: process.env.REFRESH_KEY_SALT || 'dev-refresh-salt-change-me',
    },
  },
};

export default config;

多环境部署详见多环境配置。

2. 写 Controller

// src/modules/user/controller/UserController.ts
import { Controller, Get, Post, Public, Login, Valid } from '@pylonts/fastify';
import type { FastifyRequest } from 'fastify';

@Controller('/api/users')
export class UserController {

  @Post('/')
  @Public()
  @Valid({
    body: {
      type: 'object',
      required: ['name'],
      properties: { name: { type: 'string', minLength: 1 } },
    },
  })
  async create(request: FastifyRequest) {
    // request.body 已通过 JSON Schema 校验
    return { id: '1', name: request.body.name };
  }

	@Get('/me')
	@Login()
	async profile(request: FastifyRequest) {
	  // request.token 已注入(token 鉴权通过后)
	  return request.token;
	}

	@Get('/admin')
	@Login('admin')
	async adminOnly() {
	  return { secret: 42 };
	}
  }

  // Controller 只需 request,无需 reply:
  // - 返回值自动序列化为 JSON
  // - 文件下载用 @Stream 装饰器

文件放到约定目录即自动发现,无需手动注册路由。

3. RPC 风格 Controller

加 @Rpc(name) 将类切换为 RPC 风格。无需 @Controller,无需在每个方法上加 @Post——框架自动扫描所有 public 方法,URL 由模块目录 + 控制器名 + 方法名自动推导。

import { Rpc, Login, Body } from '@pylonts/fastify';
import type { TokenObject } from '@pylonts/fastify';
import { Type } from '@sinclair/typebox';

@Rpc('users')                     // 控制器名,URL: /{module}/users/*
@Login()                          // 类级别:所有方法默认需要登录(可被方法级 @Login/@Public 覆盖)
export class UsersController {

  // POST /{module}/users/create
  @Body(Type.Object({ name: Type.String() }))
  @Response('创建用户', Type.Object({ id: Type.String() }))
  async create(body: { name: string }, token: TokenObject): Promise<{ id: string }> {
    return { id: 'u_001' };
  }

  // POST /{module}/users/login — 登录入口,覆盖类级 @Login
  @LoginEntry('users')
  @Body(Type.Object({ code: Type.String() }))
  @Response('登录', Type.Object({ token: Type.String() }))
  async login(body: { code: string }): Promise<{ token: string }> {
    return { token: 'generated-token' };
  }

  // POST /{module}/users/profile — 继承类级 @Login
  @Response('获取用户信息', Type.Object({ id: Type.String() }))
  async profile(_body: Record<string, never>, token: TokenObject): Promise<{ id: string }> {
    return { id: String(token.id) };
  }
}

RPC 方法签名 (body, token):第一个参数是已校验的请求体,第二个参数是 request.token(token 鉴权后注入的平面对象)。RPC 方法必须声明为 async(返回 Promise),框架类型 RpcHandler 强制此约束。

路由推导规则:

| 来源 | 推导逻辑 | |------|----------| | module | 模块文件夹名,可通过 route.config.ts 覆盖 | | controller | @Rpc(name) 参数,默认为类名 | | method | 方法名,保持不变 |

// src/modules/user/route.config.ts
export const module = 'u';
// → URL: /u/{controller}/{method}

urlPrefix 是完整的路径前缀段,支持多级路径。 可以包含 / 构造嵌套路由:

// src/modules/user-rpc/route.config.ts
export const urlPrefix = 'api/urpc';
// → URL: /api/urpc/{controller}/{method}

不配置时 URL 前缀默认使用模块目录名。仅在 @Rpc 控制器中生效,@Controller 不受影响。

模块级 token 隔离:route.config.ts 可配置 module,作为 @Login/@LoginEntry 缺省时的 app 名(Redis key 前缀),防止跨模块 token 滥用:

// src/modules/admin/route.config.ts
export const urlPrefix = 'api/admin';  // URL 路径(可选,默认使用模块目录名)
export const module = 'admin';          // 模块归属,兼作缺省 app 名

@Login() 的参数与模块配置的 module 是同一概念——app 名字(模块名字)。区别仅在于作用范围。优先级:方法级 > 类级 > 模块级:

| 配置 | 校验 | 作用范围 | |------|------|----------| | @Login('admin')(方法上) | token 签名 + app 归属 admin | 单个方法 | | @Login('admin')(类上) | 同上 | 整个 Controller,可被方法级覆盖 | | 模块 module:'admin' | 同上 | 整个模块,可被类/方法级覆盖 | | @Login()(无参数) | token 签名(app 名回落 x-app-key) | 方法/类 | | @Public() | 跳过签名校验 | 单个方法 | | @LoginEntry('admin') | 登录入口(无 token),app 级引导密钥验签 | 方法/类 |

| 限制 | 说明 | |------|------| | HTTP 方法 | 全部使用 @Post,不可更改 | | 参数 | 只用 @Body,不支持 @Query/@Params | | URL | 不支持路径参数 :id |

4. 服务端参数注入(__inject)

RPC handler 签名 (body, user) 中的 user 只传给 controller,默认进不了 DTO。多租户等场景需要把 user.id(或其他服务端上下文)写入请求体字段时,用 __inject 适配函数声明——框架在调用 controller 前自动执行,controller / service 无感知。

机制:在任意 TypeBox schema(基类)上挂一个不可枚举的 __inject 属性,值为 (body, user) => void 适配函数。@Body 检测到后存入路由元数据,RPC 调用前逐个执行。框架只提供机制,不内置基类——基类由项目自己定义。

// ① 项目定义注入基类(任意字段名、任意类型,适配逻辑在函数内)
// src/dto_schema/common/tenant-context.ts
import { Type } from '@sinclair/typebox';
import type { User } from '@pylonts/fastify';

export const TenantContext = Object.defineProperty(
  Type.Object({ merId: Type.Optional(Type.Number()) }),
  '__inject',
  {
    value: (body: Record<string, unknown>, user: User) => {
      body.merId = Number(user.id); // string user.id → number 字段
    },
    enumerable: false, // 非枚举:swagger / JSON 序列化 / AJV 编译全部跳过
  },
);

// ② DTO include 基类(生成器渲染为 Type.Intersect,原样保留 __inject)
// src/dto_schema/xxx/create.dto.ts
export const CreateRequest = dto
  .input({ name: dto.create().ref(merchant.name) })
  .include({ from: './tenant-context', name: 'TenantContext' })
  .as('CreateRequest');

// ③ Controller 无感知——body 已含注入字段
@Rpc('merchant')
@Login()
class MerchantController {
  @Body(CreateRequest)
  @Response('新增商户', Type.Object({ id: Type.Number() }))
  async add(body: CreateRequest, _user: User) {
    return service.add(body); // body.merId 已由框架注入
  }
}

要点:

  • __inject 值是适配函数 (body, user) => void——字段名、类型转换(如 Number(user.id))、多字段注入全部由函数负责,框架不解析任何类型
  • enumerable: false 必须设置——避免 __inject 泄漏进 swagger / JSON 序列化
  • 多个注入基类可叠加(Type.Intersect 保留各基类引用,框架递归收集所有 __inject)
  • 注入发生在 AJV 校验之后、controller 调用之前,客户端传值会被服务端值覆盖(租户安全)
  • 仅 RPC 风格生效(@Controller 的 (request, reply) 风格无 (body, user) 分离,不支持注入)

5. contextPath

部署在反向代理子路径下时,配置 contextPath,Controller 代码无需变动:

const app = await createApp({
  contextPath: '/myapp',
});
// @Controller('/api/users') → 响应地址:/myapp/api/users

搭配 nginx:

location /myapp/ {
    proxy_pass http://127.0.0.1:3000;
}

配置项

| 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | auth.token.resolveToken | (appName, token) => Promise<TokenObject \| null> | 否 | token 还原:Redis key ${appName}.${token},查不到/过期返回 null(401 code=login)。@pylonts/token-redis-driver 提供 | | auth.token.bootstrapSecrets | Record<string, string> | 否 | app 级引导密钥(登录入口无 token 时验签,初始密钥方案 2):appName → secret | | auth.token.refreshKeySalt | string | 否 | refreshToken 派生密钥盐(refresh 登录入口验签) | | auth.token.timestampWindowMs | number | 否 | 时间戳容忍窗口,默认 5 分钟 | | auth.token.nonceStore | NonceStore | 否 | 防重放存储,默认内存 MemoryNonceStore | | contextPath | string | 否 | 路径前缀,如 /myapp,Controller 代码无需变动 | | swagger | object | 否 | Swagger / OpenAPI 文档配置(title / description / version / uiPrefix) | | static | object | 否 | 静态文件服务(root / prefix),SPA fallback 到 index.html | | port / host | number / string | 否 | 监听地址(也可在 app.listen() 传入) |

多环境配置

源码保留 3 份环境配置文件作为参考,构建时只复制目标那份为 config.ts 参与编译。

目录结构

src/
├── main.ts
├── config.ts                    ← 构建时由脚本写入(.gitignore)
└── config/
    ├── development.ts            ← 源码参考:本地开发
    ├── test.ts                   ← 源码参考:测试环境
    └── production.ts             ← 源码参考:生产环境

构建脚本

{
  "scripts": {
    "build:dev":  "copy src\\config\\development.ts src\\config.ts >nul 2>&1 & npx tsc",
    "build:test": "copy src\\config\\test.ts src\\config.ts >nul 2>&1 & npx tsc",
    "build:prod": "copy src\\config\\production.ts src\\config.ts >nul 2>&1 & npx tsc"
  }
}

配置文件写法

// src/config/production.ts
import type { AppConfig } from '@pylonts/fastify';

const config: AppConfig = {
  auth: {
    token: {
      resolveToken: async (appName: string, token: string) => {
        // 用 @pylonts/token-redis-driver 的 getToken 接入
        throw new Error('resolveToken not wired');
      },
      bootstrapSecrets: {
        admin: process.env.BOOTSTRAP_SECRET_ADMIN!,  // 敏感信息从环境变量注入
      },
      refreshKeySalt: process.env.REFRESH_KEY_SALT!,
    },
  },
  swagger: false,                            // 生产环境关闭文档
};

export default config;

入口文件

// src/main.ts
import { createApp } from '@pylonts/fastify';
import config from './config';

const app = await createApp(config);
await app.listen({ port: 3000 });

Docker 部署

FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install && npm run build:prod
CMD ["node", "dist/main.js"]

敏感信息通过环境变量注入,不写在配置文件里:

# 测试环境(生产用 build:prod,方法同上)
docker run -d \
  -e BOOTSTRAP_SECRET_ADMIN=test-bootstrap \
  -e REFRESH_KEY_SALT=test-refresh-salt \
  my-app:latest

数据库与 Redis 连接(@pylonts/dao / @pylonts/token-redis-driver)同样通过环境变量注入,见各自包文档。

项目结构

src/
├── main.ts
└── modules/
    └── user/
        ├── controller/UserController.ts   ← @Controller/@Get/@Post 声明路由
        ├── service/UserService.ts         ← 纯业务流程编排
        ├── dao/
        │   └── UserDao.ts                 ← knex 查询(@pylonts/dao)
        ├── dto/
        │   ├── CreateUserRequest.ts
        │   └── UserResponse.ts
        ├── entity/UserEntity.ts
        └── enums/UserStatus.ts

装饰器

| 装饰器 | 作用 | |--------|------| | @Controller('/prefix') | 声明路由前缀,类必须 export | | @Get('/path') | GET 路由 | | @Post('/path') | POST 路由 | | @Put('/path') | PUT 路由 | | @Delete('/path') | DELETE 路由 | | @Login() | 需要 token 签名验证 + 登录身份(security-only 匿名 token 拒绝)。支持类级别(作用于所有方法)和方法级别(覆盖类级设置) | | @Login('admin') | 需要 token 签名 + app 归属 admin(app 名字 = 模块名字)+ 登录身份。支持类级别和方法级别 | | @LoginEntry('admin') | 标记 app 的登录入口(login/refresh,无 token),app 级引导密钥 / refreshToken 派生密钥验签 | | @Public() | 跳过签名校验(公开接口) | | @Valid(schema) | 绑定 Fastify JSON Schema 校验 | | @Body(def) | 声明式请求体校验 | | @Query(def) | 声明式查询参数校验 | | @Params(def) | 声明式路径参数校验 | | @Response(description, def, statusCode?) | 声明响应 Schema,用于 OpenAPI 文档生成,默认 200 | | @Rpc(name?) | RPC 风格控制器。无需 @Controller,方法自动扫描,签名 (body, user)。name 覆盖控制器名,默认类名。 | | @Stream(description?) | 流式下载端点,方法返回 StreamResponse { name, content }。description 注入 OpenAPI 响应描述。 | | @File(description?) | 文件下载端点,方法返回 FileResponse { name, path }。description 注入 OpenAPI 响应描述。 | | @Multipart() | 标记 multipart/form-data 文件上传端点。替代 @Body(输入层),仍需 @Response(输出层)。 |

声明式校验

比手写 JSON Schema 更简洁的类型工具:

import {
  Body, Query, Params,
  String, Number, Boolean, Email, Required, ArrayOf, Enum, Min, Max,
} from '@pylonts/fastify';

@Controller('/api/users')
class UserController {

  @Post('/')
  @Body({
    name: Required(String),
    email: Required(Email),
    age: Number,
    tags: ArrayOf(String),
    status: Enum(['active', 'inactive']),
  })
  create(request: FastifyRequest) {
    // request.body 已校验
  }

  @Get('/')
  @Query({ page: Number, keyword: String })
  list(request: FastifyRequest) { ... }

  @Get('/:id')
  @Params({ id: Required(String) })
  show(request: FastifyRequest) { ... }
}

@Body/@Query/@Params 自动生成标准 JSON Schema,与 @Valid 等价但可读性更高。

Swagger / OpenAPI

配置 swagger 即可自动生成 OpenAPI 文档,路由、参数、Schema 全部自动提取:

const app = await createApp({
  swagger: {
    title: 'My API',
    description: 'API 文档',
    version: '1.0.0',       // 可选,默认读取 package.json version
    uiPrefix: '/docs',       // 可选,默认 /docs
  },
});

启动后访问 /docs 即可浏览 Swagger UI,/docs/json 获取 OpenAPI JSON。

配置 contextPath 时自动联动,例如 contextPath: '/myapp' → Swagger UI 在 /myapp/docs。

依赖自动补充 @fastify/swagger + @fastify/swagger-ui。

@Response — 声明响应 Schema

@Response(description, def, statusCode?) 声明接口的响应结构,写入路由的 OpenAPI responses,不参与运行时校验。def 引用 DTO 而非内联——DTO 由 dto_schema DSL 生成(npm run dto:gen → api-dto/src/**/*.dto.gen.ts):

import { Rpc, Login, Body, Response } from '@pylonts/fastify';
import type { User } from '@pylonts/fastify';
import {
  BdListRequest,
  BdListResponse,
  BdRow,
  BdDetail,
} from '@your-app/api-dto/admin/bd';
import { EmptyBody } from '@pylonts/core';

@Rpc('bd')
class BdController {

  @Login()
  @Body(BdListRequest)
  @Response('BD列表', BdListResponse)
  list(body: BdListRequest, user: User): Promise<BdListResponse> { ... }

  @Login()
  @Body(BdAddRequest)
  @Response('新增BD', BdRow)
  add(body: BdAddRequest, user: User): Promise<BdRow> { ... }

  // 非 200 状态码 — 第三个参数指定 statusCode
  @Response('校验失败', SomeErrorResponse, 422)
  validate(body: SomeRequest, user: User): Promise<SomeErrorResponse> { ... }
}

description 缺省时按状态码使用默认文案(200 → OK、422 → Unprocessable Entity 等)。

@Stream / @File 内部写入同一 response 槽位(content type 为 application/octet-stream),与 @Response 互斥,不要叠加使用(后写覆盖先写)。

统一响应

Happy path:Controller return data,原样输出 JSON,不做包装。

{ "id": "1", "name": "张三" }

Error path:自动捕获异常,统一格式:

HTTP 401 ← token 鉴权失败 / 签名失败
HTTP 403 ← 角色权限不足
HTTP 404 ← 路由不存在
HTTP 422 ← BusinessException、NotFoundException、Schema 校验失败
HTTP 500 ← ServiceException、未预期异常(message 通用化,不暴露堆栈)

业务/校验异常:

{ "error": "手机号已存在" }

鉴权异常:

{ "code": "login", "error": "Authorization token is invalid" }
{ "code": "error", "error": "签名验证失败" }
{ "code": "retry", "error": "凭证无效或已过期" }

异常

import { BusinessException, NotFoundException, ServiceException } from '@pylonts/fastify';

throw new BusinessException('库存不足');      // → 422  { error: '库存不足' }
throw new NotFoundException('用户不存在');     // → 422  { error: '用户不存在' }
throw new ServiceException('推送失败');        // → 500  { error: '服务器内部错误' }

Token 鉴权

统一 token 鉴权链(TokenStrategy),替代旧 HMAC + JWT 双层体系。所有接口默认需要签名验证(@Public() 标记的接口跳过签名)。@Login() 标记的接口在签名基础上要求登录身份(token 对象必须含 identity 字段,security-only 匿名 token 拒绝)并限定 app 归属。无任何标注的接口 = 匿名浏览:仅验签(x-token + 对象 secret),匿名 sign token 可通过,不要求身份。

三种验证模式

| 模式 | 判定 | 验签密钥 | 身份要求 | |------|------|---------|---------| | 无标注接口(匿名浏览) | 请求头带 x-token | resolveToken(appName, token) 还原 Redis 平面对象,取 object.secret | ❌(匿名 sign token 可过) | | @Login 业务接口 | 请求头带 x-token | 同上 | ✅(token 必须含 identity 字段) | | login 入口(@LoginEntry,无 token) | body 无 refreshToken | app 级引导密钥 bootstrapSecrets[appName](初始密钥方案 2) | 登录入口 | | refresh 入口(@LoginEntry,无 token) | body 携带 refreshToken | deriveRefreshKey(refreshToken, refreshKeySalt)——能正确签名 == 持有 refreshToken | 登录入口 |

app 名解析:@Login(module) / @LoginEntry(app) 的参数,缺省回落 x-app-key 请求头。

@Public() 只允许出现在第三方回调 controller(src/modules/{thirdApi}/,外部系统不持有平台密钥,第三方自身验签由业务手写)——pylonts lint controller 强制此白名单。

token 按 app 隔离——Redis key 前缀 {appName}.{token},admin 的 token 无法访问其他 app 的接口。

服务端配置

const app = await createApp({
  auth: {
    token: {
      // token 还原(必配,否则业务路由 500)
      resolveToken: async (appName: string, token: string) => {
        const obj = await tokenStore.get(`${appName}.${token}`);
        return obj ?? null;   // null → 401 { code: 'login', error: '登录已过期' }
      },
      // app 级引导密钥(login 入口验签)
      bootstrapSecrets: { admin: 'dev-bootstrap-secret' },
      // refreshToken 派生密钥盐(refresh 入口验签)
      refreshKeySalt: 'dev-refresh-salt',
      timestampWindowMs: 5 * 60 * 1000,  // 可选,默认 5 分钟
    },
  },
});

resolveToken 可对接任意存储(Redis 用 @pylonts/token-redis-driver 的 getToken)。

路由使用

默认所有接口需签名,request.token 为还原的平面对象:

@Controller('/api/app')
export class AppController {

  @Get('/data')
  async data(request: FastifyRequest) {
    return { clientId: request.token.appKey };
  }

  @Get('/profile')
  @Login()
  async profile(request: FastifyRequest) {
    // 签名通过 + token 校验通过 → request.token 为平面对象
    return request.token;
  }
}

签名/鉴权失败统一返回 401 { code, error }:

  • code: 'error' — 不可重试(签名不匹配、缺少参数、timestamp/nonce 异常)
  • code: 'login' — token 缺失/过期,客户端跳转登录
  • code: 'retry' — 签名凭据无效,客户端刷新后重试

服务端日志会详细记录失败原因和客户端数据。支持 nonce 防重放(默认内存 MemoryNonceStore,可注入自定义 NonceStore)、timestamp 时间窗口校验、constant-time 签名比对。

文件上传

上传时签名逻辑不变:将 JSON 元数据签名后以 _sign_body 字段嵌入 multipart/form-data,文件以 file 字段附带:

@Controller('/api/app')
class AppController {

  @Post('/upload')
  async upload(request: FastifyRequest) {
    const body = request.body as Record<string, unknown>;
    // body.file → { type: 'file', fieldname, filename, mimetype, file, _buf }
    // body._sign_body → { type: 'field', value: '{"fileName":"a.png",...}' }
  }
}

文件下载

import { Controller, Get, Stream } from '@pylonts/fastify';
import type { StreamResponse } from '@pylonts/fastify';

@Controller('/api/report')
class ReportController {

  @Get('/export')
  @Stream('导出CSV报表')
  async exportCSV(): Promise<StreamResponse> {
    const csv = ['name,phone', '张三,138xxxx'].join('\n');
    return { name: 'report.csv', content: csv };
  }
}

Controller 只需返回 { name, content },框架自动设置 Content-Disposition 并流式返回。无需 reply 参数。

content 支持 string 和 Buffer,适用 CSV / Excel / 图片等任意二进制数据。

Token 工具

deriveRefreshKey — refreshToken 派生密钥

import { deriveRefreshKey } from '@pylonts/fastify';

const key = deriveRefreshKey(refreshToken, refreshKeySalt);

HMAC(refreshToken, 固定盐)——能正确签名 == 持有 refreshToken,签名与凭据一体、每用户独立。

DB 工具(@pylonts/dao)

数据库访问已从框架拆出,由 @pylonts/dao 提供(knex 实例 + 连接池工厂 + 迁移):

import { knex } from '@pylonts/dao';

// 查多行
const users = await knex('users').select('*').where({ status: 0 });

// 查单行
const user = await knex('users').where({ id }).first();

// 增删改
await knex('users').update({ name: '李四' }).where({ id });

无需手动 getConnection / release,连接池由工厂自动管理。迁移工具见 @pylonts/dao 的 migrate。

Redis Token 存储(@pylonts/token-redis-driver)

token 还原已拆到 @pylonts/token-redis-driver:

import { initRedisClient } from 'ioredis';
import { initTokenRedis, createToken, getToken } from '@pylonts/token-redis-driver';

const redis = new Redis({ host: '127.0.0.1', port: 6379 });
initTokenRedis(redis);   // 应用启动时注册

const { token } = await createToken('admin', { secret, cipher });   // login 写侧
const obj = await getToken('admin', token);                          // 请求读侧
  • initTokenRedis(redis) — 注册 ioredis 客户端(启动时)
  • createToken / attachIdentity / rotateToken / deleteToken / getToken — token 生命周期
  • key = {appName}.{token},TTL 30 分钟,纯随机 hash(256-bit hex)

token 还原接入框架:

auth: {
  token: {
    resolveToken: getToken,   // (appName, token) → TokenObject | null
    bootstrapSecrets: { admin: '...' },
    refreshKeySalt: '...',
  },
}

分页

import type { PageResult } from '@pylonts/fastify';

@Get('/')
async list(): Promise<PageResult<UserResponse>> {
  return {
    items: [...],
    total: 100,
    page: 1,
    pageSize: 20,
  };
}

枚举规范

// DB number → 数值枚举
export enum UserStatus {
  NORMAL,   // 0
  LOCKED,   // 1
}

// DB string → 字符串枚举
export enum OrderType {
  NORMAL = 'NORMAL',
  RUSH = 'RUSH',
}

所见即所得,无额外 code/description 字段。

二次开发

框架设计为可扩展,以下是在其之上做二次开发的常见模式。

定制校验错误消息

框架内置 errorRules,覆盖 24 个 JSON Schema 关键字的默认中文提示。可通过 addRule 覆盖:

import { addRule } from '@pylonts/fastify';

// 全局生效,框架启动前调用一次即可
addRule('required', (field) => `${field} 是必填的`);
addRule('pattern',  (field) => `${field} 格式不对`);
addRule('type',     (field) => `${field} 类型不匹配`);

不调用则使用内置默认(password 必填、phone 格式错误、age 类型错误 等)。未覆盖的关键字回退到 AJV 原始英文。

完整的关键字列表见 src/common/validation/errorRules.ts。

自定义 AJV 校验关键字

标准 JSON Schema 关键字不够用时,通过 addKeyword 注册自己的规则:

// src/main.ts — createApp 之前调用
import Ajv from 'ajv';

const ajv = new Ajv();
ajv.addKeyword({
  keyword: 'isPhone',
  validate: (_schema, data) => /^1[0-9]{10}$/.test(data),
  errors: true,
  error: {
    message: (cxt) => `${cxt.instancePath.replace(/^\//, '')} 手机号格式不正确`,
  },
});

然后在 Controller 中使用:

// Typebox 风格
const Request = Type.Object({
  phone: Type.String({ isPhone: true }),
});

// SchemaProp 风格
@Body({ phone: { type: 'string', isPhone: true } })

关键字注册时自带的 error.message 会自动被 errorRules.formatError 使用,无需额外配置。

自定义 SchemaProp 辅助

在 schema.ts 中新增类型辅助,在项目中任何 Controller 直接使用:

// src/common/validation/schema.ts
export const Phone: SchemaProp = { type: 'string', pattern: '^1[0-9]{10}$' };

export const IdCard: SchemaProp = {
  type: 'string',
  pattern: '^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$',
};
// Controller
@Body({
  phone: Required(Phone),
  idCard: Required(IdCard),
})

自定义 resolveToken

token 还原可对接任何存储系统(Redis、DB、内存):

import { knex } from '@pylonts/dao';

createApp({
  auth: {
    token: {
      resolveToken: async (appName: string, token: string) => {
        const row = await knex('tokens').where({ app_name: appName, token }).first();
        return row
          ? { secret: row.secret, ...JSON.parse(row.payload) }
          : null;
      },
    },
  },
});

自定义 NonceStore

签名防重放实现可替换(默认 MemoryNonceStore 为单实例内存实现,多实例部署必须注入共享存储,否则各实例 nonce 相互独立、重放防护失效):

import { RedisNonceStore } from '@pylonts/sign-redis-driver';

class MyNonceStore {
  async checkAndSet(nonce: string, ttlSeconds: number): Promise<boolean> { /* ... */ }
}

createApp({
  auth: {
    token: {
      resolveToken,
      nonceStore: new RedisNonceStore(),   // 多实例共享(Redis SET NX EX)
      // nonceStore: new MyNonceStore(),   // 或自定义实现
    },
  },
});

自定义 AuthStrategy

TokenStrategy 已覆盖 token 体系全部验证模式(业务 / login 入口 / refresh 入口)。对接 Session、OAuth 等非 token 鉴权时,实现 AuthStrategy 并替换工厂:

import type { AuthStrategy, TokenObject } from '@pylonts/fastify';

class SessionStrategy implements AuthStrategy {
  async authenticate(request: FastifyRequest, _reply: FastifyReply): Promise<TokenObject> {
    const sessionId = request.cookies.session;
    // 用 @pylonts/dao 的 knex 或任意存储查询会话
    const user = await yourSessionStore.get(sessionId);
    if (!user) throw new AuthException('会话已过期');
    return { id: user.id, secret: 'session-secret' };
  }
}

自定义 Controller 元数据扫描

框架通过 Reflect.defineMetadata / getMetadata 管理路由元数据。装饰器和扫描逻辑在 src/common/scanner/ 中,侵入式修改可参考:

  • decorators.ts — 新增装饰器(如 @RateLimit、@Cache)
  • index.ts — 扫描和路由注册(scanControllers)

框架内置的 @Response、@Stream 均按此模式实现,是添加新装饰器的范本。

新增装饰器示例:

const RATE_LIMIT_META = Symbol('route:rateLimit');

export function RateLimit(max: number, windowMs: number): MethodDecorator {
  return (target, propertyKey) => {
    Reflect.defineMetadata(RATE_LIMIT_META, { max, windowMs }, target, propertyKey);
  };
}
// scanner/index.ts 中读取
const rateLimitMeta = Reflect.getMetadata(RATE_LIMIT_META, exported, route.handlerName);
if (rateLimitMeta) {
  routeDef.preHandler = [/* rate limiter hook */];
}

数据库迁移(@pylonts/dao)

迁移能力已拆到 @pylonts/dao 的 migrate(工厂创建连接后执行 migrations/*.sql,追踪已执行文件)。框架本身不执行 DDL。

import { createPool, migrate } from '@pylonts/dao';

const pool = createPool({ /* mysql config */ });
await migrate(pool, path.join(projectRoot, 'src/migrations'));

依赖

  • Fastify 5
  • @fastify/multipart(文件上传)
  • @fastify/static(静态文件,可选)
  • @fastify/swagger + @fastify/swagger-ui(API 文档,可选)
  • reflect-metadata(装饰器元数据)
  • fast-glob(Controller 自动扫描)

token 还原 / DB 为外部可选依赖:@pylonts/token-redis-driver、@pylonts/dao。

TypeScript experimentalDecorators 和 emitDecoratorMetadata 必须启用。

参考

待实现

| 特性 | 说明 | |------|------| | CORS | @fastify/cors,跨域支持 | | 安全头 | @fastify/helmet,CSP / HSTS / X-Frame-Options 等 | | 请求 ID / 链路追踪 | 为每个请求注入 trace-id,日志可串联 | | 限流 | 基于令牌桶或滑动窗口的速率限制 | | 缓存抽象 | 防穿透(空值缓存)、防击穿(单飞锁)、防雪崩(随机 TTL) |