跳到主要内容

在 NestJS 中使用 Keq

本文介绍如何在 NestJS 项目中集成 Keq,以及如何使用 OpenAPI 自动生成类型安全的客户端代码。

安装依赖

注册 KeqModule

在应用的根模块中导入 KeqModuleKeqModule 是一个 @Global() 模块,提供全局的 KeqRequest 实例和 KeqMiddlewareConsumer

src/app.module.ts
import { Module } from '@nestjs/common'
import { KeqModule } from '@keq-request/nestjs'

@Module({
  imports: [
    KeqModule,                                                

    /* ...其他模块... */
  ]
})
export class AppModule {}

注册后即可在 Service 中通过依赖注入使用 KeqRequest

src/app.service.ts
import { Injectable } from '@nestjs/common'
import { KeqRequest } from 'keq'

@Injectable()
export class AppService {
  constructor(private readonly request: KeqRequest) {}

  async getCats() {
    const cats = await this.request.get('https://api.example.com/cats')
    return cats
  }
}

中间件进阶配置

概述

Keq 提供了类似 NestJS MiddlewareConsumer 的编程式接口,支持按路由注册中间件以及排除特定路由

中间件体系中有两种消费者(Consumer):

  • KeqMiddlewareConsumer:全局消费者,由 KeqModule 提供。通过它注册的中间件对所有 KeqRequest 发出的请求生效。
  • KeqConsumer<T>:模块级消费者,通过 @InjectKeqConsumer(ModuleClass) 注入,绑定到特定的生成模块。通过它注册的中间件仅对该模块的请求生效。

两种消费者共用同一套中间件 API(apply()forRoutes()exclude() 和链式注册),详见下文各小节。

中间件执行顺序

请求发送时,中间件按以下顺序逐层执行:

全局中间件register() 中的模块级中间件通过 KeqConsumer 注册的中间件

无论使用哪种配置方式,这个三层顺序始终不变。

模块级中间件有两种配置方式,分别适用于不同场景:

方式适用场景
register({ middlewares: [...] })模块导入时静态配置,中间件在模块初始化时即生效
@InjectKeqConsumer()在其他 Service 中动态注册,支持依赖注入 NestJS Provider(如 AuthServiceConfigService

两种方式的详细用法和对比请参见第 4.3 节

全局中间件 — KeqMiddlewareConsumer

KeqMiddlewareConsumer 是一个 @Injectable() 类,可直接注入到任意 Service 或 Controller 中。通过它注册的中间件会对所有 KeqRequest 发出的请求生效:

src/global-middleware.service.ts
import { Injectable } from '@nestjs/common'
import { KeqMiddlewareConsumer, KEQ_ROUTES } from '@keq-request/nestjs'
import { validateStatusCode } from '@keq-request/exception'

@Injectable()
export class GlobalMiddlewareService {
  constructor(consumer: KeqMiddlewareConsumer) {
    // 全局应用 validateStatusCode 中间件
    consumer.apply(validateStatusCode()).forRoutes(KEQ_ROUTES.ALL)
  }
}
注入时机

KeqMiddlewareConsumerKeqModule 全局提供,只要模块导入了 KeqModule(或任何依赖了 KeqModule 的模块),即可在任何地方注入。

apply() — 注册中间件

apply() 方法接收 Keq 中间件函数或 NestJS 风格的中间件类:

import { type KeqNestMiddleware, type KeqExecutionContext, type KeqNext } from '@keq-request/nestjs'

// NestJS 风格中间件类(支持依赖注入)
class AuthMiddleware implements KeqNestMiddleware {
  use(ctx: KeqExecutionContext, next: KeqNext): Promise<void> {
    // 鉴权逻辑
    return next()
  }
}

consumer.apply(AuthMiddleware, loggingMiddleware).forRoutes(KEQ_ROUTES.ALL)
中间件类型

apply() 支持两种类型:

  • KeqMiddleware:普通函数中间件 (ctx, next) => void
  • KeqNestMiddleware:NestJS 类中间件(支持依赖注入,可注入其他 NestJS Provider)

forRoutes() — 指定路由范围

forRoutes() 控制中间件对哪些请求生效:

// 全局生效
consumer.apply(loggingMiddleware).forRoutes(KEQ_ROUTES.ALL)

// 仅对特定路径生效(支持 picomatch 通配符)
consumer.apply(authMiddleware).forRoutes({
  pathname: '/api/**',  // 仅匹配 /api/* 路径
  method: 'POST',       // 仅 POST 请求
})

forRoutes() 接收两种路由目标:

类型说明
KEQ_ROUTES.ALL匹配所有请求
KeqRouteInfo{ host?, method?, pathname? } — 按 host / 方法 / 路径过滤
模块级路由

如需按模块(而非路由)限定中间件作用范围,请使用模块级消费者 KeqConsumer<T>,详见第 4.3 节

路由匹配

KeqRouteInfo 使用 picomatch 作为路径匹配引擎。当同时指定多个条件时(如 pathname + method),它们为 AND 关系,即所有条件都满足时才命中。

exclude() — 排除特定路由

exclude() 可在 forRoutes() 之前调用,排除中间件作用范围内的特定路由:

// 全局应用 auth 中间件,但排除 health check 和公开接口
consumer.apply(AuthMiddleware)
  .exclude(
    { pathname: '/health' },
    { pathname: '/public/**' },
    { method: 'OPTIONS' },
  )
  .forRoutes(KEQ_ROUTES.ALL)

也可以在路由级中间件中使用,实现子路径排除:

// 对 /api/** 应用日志中间件,但排除 /api/internal/**
consumer.apply(LoggingMiddleware)
  .exclude({ pathname: '/api/internal/**' })
  .forRoutes({ pathname: '/api/**' })
exclude 语义
  • 单条 KeqRouteInfo 内的多个条件(如 { pathname: '/api/**', method: 'GET' })为 AND 关系
  • 多次调用 exclude() 或多条参数OR 累积关系(任何一条匹配即排除)
  • exclude() 返回 config proxy,支持在 forRoutes() 之前链式调用

链式注册

KeqMiddlewareConsumerKeqConsumer<T> 均支持链式注册多个中间件规则:

@Injectable()
export class AppMiddleware {
  constructor(consumer: KeqMiddlewareConsumer) {
    consumer
      .apply(validateStatusCode())
      .forRoutes(KEQ_ROUTES.ALL)

    consumer
      .apply(AuthMiddleware)
      .exclude({ pathname: '/public/**' })
      .forRoutes({ pathname: '/api/**' })

    consumer
      .apply(LoggingMiddleware)
      .forRoutes(KEQ_ROUTES.ALL)
  }
}

使用 OpenAPI 生成 NestJS 模块

@keq-request/cli 支持从 OpenAPI 文档生成 NestJS 模块,提供完整的类型定义和依赖注入支持。

配置生成模式

.keqrc.ts 中设置 modenestjs-module

.keqrc.ts
import { defineKeqConfig, FileNamingStyle } from '@keq-request/cli'

export default defineKeqConfig({
  mode: 'nestjs-module',   
  outdir: "./src/apis",
  rendering: {
    fileNamingStyle: FileNamingStyle.snakeCase,
  },
  modules: {
    catService: "./cat-service-swagger.json",
  },
})

运行生成命令:

注册生成的模块

将生成的模块导入到应用中:

src/app.module.ts
import { Module } from '@nestjs/common'
import { KeqModule } from '@keq-request/nestjs'
import { CatServiceModule } from './apis/cat_service/cat_service.module'

@Module({
  imports: [
    // 全局 KeqModule(提供 KeqRequest 和 KeqMiddlewareConsumer)
    KeqModule,

    // 注册生成的模块(fork 自全局 KeqRequest,继承全局中间件)
    CatServiceModule.register({                                   
      // 模块级中间件(全局中间件之后执行,详见 3.1 节)
      middlewares: [                                              
        setBaseUrl('https://cat-api.example.com'),                
        appendHeader('Authorization', 'Bearer YOUR_TOKEN_HERE'),  
      ]                                                           
    }),                                                           
  ]
})
export class AppModule {}

模块级中间件的两种配置方式

为生成的模块配置模块级中间件,有静态配置(register())和动态注册(@InjectKeqConsumer())两种方式。无论使用哪种方式,中间件都遵循三层执行顺序:全局中间件 → register() 中间件 → KeqConsumer 中间件(详见3.1 节)。

方式一:register() 静态配置

在模块导入时通过 register({ middlewares: [...] }) 直接配置。适用于配置在模块导入时已知、不依赖其他 NestJS Provider 的场景:

CatServiceModule.register({
  middlewares: [
    setBaseUrl('https://cat-api.example.com'),
    appendHeader('Authorization', 'Bearer YOUR_TOKEN_HERE'),
  ],
})

方式二:@InjectKeqConsumer() 动态注册

当中间件需要依赖注入其他 NestJS Provider(如 AuthServiceConfigService 等)时,使用 @InjectKeqConsumer() 在独立的 Service 中动态注册:

src/cat-service.middleware.ts
import { Injectable } from '@nestjs/common'
import { InjectKeqConsumer, KeqConsumer } from '@keq-request/nestjs'
import { CatServiceModule } from './apis/cat_service/cat_service.module'

@Injectable()
export class CatServiceMiddleware {
  constructor(
    @InjectKeqConsumer(CatServiceModule) consumer: KeqConsumer<typeof CatServiceModule>,
  ) {
    // 该中间件仅对 CatServiceModule 的请求生效
    consumer.apply(authMiddleware).forRoutes('*')
  }
}
注入条件

@InjectKeqConsumer() 要求目标模块类具有 KEQ_CONSUMER 静态属性。由 @keq-request/cli 生成的 NestJS 模块自动包含此属性,可直接用于注入。

两种方式对比

方式适用场景依赖注入
register({ middlewares: [...] })模块导入时静态配置,中间件在模块初始化时即生效不支持
@InjectKeqConsumer()在其他 Service 中动态注册,中间件在 Service 实例化时生效支持(可注入 AuthServiceConfigService 等)

隔离模式

如果你不希望生成的模块继承全局中间件,可以设置 isolate: true 创建独立的 KeqRequest 实例:

@Module({
  imports: [
    CatServiceModule.register({
      isolate: true,   // 不继承全局中间件,创建独立实例
      middlewares: [
        setBaseUrl('https://cat-api.example.com'),
        appendHeader('Authorization', 'Bearer YOUR_TOKEN_HERE'),
      ]
    }),
  ]
})
export class SomeFeatureModule {}
实例获取策略
  • 未设置 isolate(默认):从全局 KeqModule 提供的 KeqRequest fork,继承全局中间件
  • isolate: true:创建全新的 KeqRequest 实例,不继承任何全局配置

如果未注册 KeqModule 且未设置 isolate: true,模块会自动创建独立实例并输出警告。

使用生成的客户端

src/cat.service.ts
import { Injectable } from '@nestjs/common'
import { CatServiceClient } from './apis/cat_service/cat_service.client'

@Injectable()
export class CatService {
  constructor(private readonly catServiceClient: CatServiceClient) {}

  async getCats() {
    const response = await this.catServiceClient.getCats<200>()
    return response.body
  }

  async getCatById(id: string) {
    const response = await this.catServiceClient.getCatById<200>({ id })
    return response.body
  }
}