- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
在 LoopBack 4(loopback-next)中,code-first 是一种“自底向上”的 API 构建方式:不预先编写 OpenAPI 文档,而是直接用 TypeScript 类和装饰器声明模型与控制器方法,框架在运行时自动收集元数据、拼装出合法的 OpenAPI 3.0 规范,并据此完成请求路由与校验。读完本文,你将掌握如何用@model()/@property()定义模型元数据、用@operation()/@param()/@requestBody()定义路由,以及理解这些装饰器如何被RestServer转换为/openapi.json、/openapi.yaml端点和运行时路由逻辑。
适用场景:什么时候选择 code-first
根据官方文档 Defining-the-API-using-code-first-approach 的说明,当你符合以下任一情况时,适合采用 code-first 自底向上地构建应用:
- 对你现有工具链能提供的能力还没有完整的了解;
- 希望把已经存在的领域模型(domain models)捕获下来,并以 API 的形式对外暴露;
- 需要在初始实现之上让 API 持续增长和演进;
- 希望在生产环境的早期阶段就搭建并运行 API,以便尽早看清最终产品的全貌。
LoopBack 提供了一整套基于TypeScript 实验性装饰器(experimental decorator)的工具:用装饰器采集代码的metadata,再将其组装成一份合法的 OpenAPI 规范,作为 API 的描述。其中两类核心构件及其装饰器是:
- Model:
@model()、@property() - Routes:
@operation()、@param()(以及@requestBody())
下面以 Todo 应用为例完整走一遍这条链路,示例模型可参考 Todo 模型教程。
第一步:用 @model 与 @property 定义模型
模型是 API 层与数据源层之间的公共定义。由于 API 最终是围绕模型及其属性的操作构建的,所以模型通常是第一个被定义的东西。
从一个普通 TypeScript 类开始
先写一个描述模型及其属性的简单类(原文档中的最简形态):
export class Todo { id?: number; title: string; desc?: string; isComplete: boolean; }用装饰器生成模型元数据
在上述表示上加上@model与@property装饰器,即可形成模型的metadata(即 model definition):
import {model, property} from '@loopback/repository'; @model() export class Todo { @property() id?: number; @property({ required: true, }) title: string; @property() desc?: string; @property() isComplete: boolean; }这份模型定义会被 LoopBack 及其扩展用于多种用途:
- 为 API 生成 OpenAPI schema;
- 在请求/响应生命周期中校验模型的实例;
- 在数据源操作期间自动推断模型之间的关系(relations)。
源码视角:装饰器如何变成 ModelDefinition
在 model.decorator.ts 中可以确认上述行为的底层实现:
@model()本质是MODEL_KEY(元数据键为'loopback:model')上的类装饰器工厂,它把定义对象写入元数据,随后调用buildModelDefinition()构建完整的ModelDefinition并挂到构造函数上(见 model.decorator.ts#L42-L59)。@property()是属性装饰器工厂,把Partial<PropertyDefinition>写入MODEL_PROPERTIES_KEY('loopback:model-properties')(见 model.decorator.ts#L125-L131)。buildModelDefinition()会遍历原型上的全部属性元数据,优先使用显式声明的type,否则回退到 TypeScript 的 design-time type 推断;如果两者都拿不到,会抛出CANNOT_INFER_PROPERTY_TYPE错误(见 model.decorator.ts#L86-L98)。这意味着:在noImplicitAny等严格编译配置下,可选属性可能推断不出类型,此时建议在@property中显式写type(如type: 'number')。- 同一文件还提供
property.array(itemType, definition),用于为数组属性声明元素类型,例如@property.array(Todo)(见 model.decorator.ts#L145-L165)。 - 声明了
hidden: true的属性会被收集进settings.hiddenProperties,从而不出现在公开的 schema 中(见 model.decorator.ts#L100-L104)。
更完整的实战写法:Todo 示例模型
仓库中的 examples/todo/src/models/todo.model.ts 展示了实际项目里更完整的写法:继承Entity基类、显式type、id/generated标记,以及构造函数接收部分数据:
import {Entity, model, property} from '@loopback/repository'; @model() export class Todo extends Entity { @property({ type: 'number', id: true, generated: false, }) id?: number; @property({ type: 'string', required: true, }) title: string; @property({ type: 'string', }) desc?: string; @property({ type: 'boolean', }) isComplete?: boolean; constructor(data?: Partial<Todo>) { super(data); } }如果不想手写,也可以用 CLI 生成:lb4 model命令会交互式询问类名、基类(如Entity)及每个属性的类型、是否 ID、是否必填、默认值,最终生成src/models/todo.model.ts并更新src/models/index.ts,完整交互过程见 todo-tutorial-model。
第二步:用 @operation、@param、@requestBody 定义路由
模型定义完成后,创建一个控制器(controller)来承载每个 API paths 的路由方法。
未装饰的控制器方法
import {Todo} from '../models/todo.model'; export class TodoController { constructor() {} async createTodo(todo: Todo) { // 数据创建逻辑写在这里 } async findTodoById(id: number, items?: boolean): Promise<Todo> { // 数据查询逻辑写在这里 } // ... }此时路由方法尚不包含任何“属于哪个 API 端点”的信息。为每个方法追加@operation(或其简写@get/@post等),为参数追加@param或@requestBody:
import {Todo} from '../models/todo.model'; import {post, get, param, requestBody} from '@loopback/rest'; export class TodoController { constructor() {} @post('/todos') // 等价于 @operation('post', '/todos') async createTodo(@requestBody() todo: Todo) { // 数据创建逻辑写在这里 } @get('/todos/{id}') async findTodoById( @param.path.number('id') id: number, @param.query.boolean('items') items?: boolean, ): Promise<Todo> { // 数据查询逻辑写在这里 } // ... }源码视角:路由装饰器的实现
@get/@post/@put/@patch/@del都只是对operation(verb, path, spec?)的封装,最终通过MethodDecoratorFactory把{verb, path, spec}写入OAI3Keys.METHODS_KEY(见 operation.decorator.ts#L19-L89)。第三个参数spec允许内联提供完整的OperationObject(responses、description 等)。@param提供了path、query、header、cookie四个位置工厂,每个位置下有string、number、boolean、integer、date、dateTime等类型化方法,如@param.path.number('id');此外还有面向查询过滤的快捷方法@param.filter(Model)与@param.where(Model)(见 parameter.decorator.ts)。@requestBody()接受一个可选的Partial<RequestBodyObject>:若不提供content,默认按application/json处理,并且如果媒体类型中没有schema,装饰器会根据参数类型自动生成——前提是该参数类型是用@model装饰过的类(见 request-body.decorator.ts#L16-L79)。
更完整的实战写法:Todo 控制器示例
仓库中 examples/todo/src/controllers/todo.controller.ts 展示了结合responses、getModelSchemaRef()与仓储注入的完整 REST 控制器,是原文档简化示例的直接落地形态:
@post('/todos', { responses: { '200': { description: 'Todo model instance', content: { 'application/json': {schema: getModelSchemaRef(Todo)}, }, }, }, }) async create( @requestBody({ content: { 'application/json': { schema: getModelSchemaRef(Todo, { title: 'NewTodo', exclude: ['id'], }), }, }, }) todo: Omit<Todo, 'id'>, ): Promise<Todo> { return this.todoRepository.create(todo); } @get('/todos/{id}', { responses: { '200': { description: 'Todo model instance', content: { 'application/json': { schema: getModelSchemaRef(Todo, {includeRelations: true}), }, }, }, }, }) async findById( @param.path.number('id') id: number, @param.filter(Todo, {exclude: 'where'}) filter?: FilterExcludingWhere<Todo>, ): Promise<Todo> { return this.todoRepository.findById(id, filter); }可以看到几个工程化要点:用getModelSchemaRef(Todo, {exclude: ['id']})为“新建”请求体复用模型 schema 但剔除只读字段;用@param.filter(Todo)免费获得带完整 Filter schema 的filter查询参数;在@post的第二参中声明responses以让生成的 OpenAPI 规范包含响应描述。
第三步:装饰器元数据如何变成 OpenAPI 规范
当路由都被装饰完毕后,应用就具备了对外提供 API 的条件。运行RestServer实例时,一份描述整个应用 API 的 OpenAPI 规范会被构建出来;该规范完全由装饰元素的元数据生成,并反过来为运行中的应用提供路由逻辑。
源码层面,这条链路的入口在 controller-spec.ts 的resolveControllerSpec():它从控制器类与方法级元数据(OAI3Keys.CLASS_KEY、OAI3Keys.METHODS_KEY、参数与请求体元数据等)解析出ControllerSpec(basePath+paths+ 由模型元数据生成的components),并支持类级@api()预定义规范、@oas.tags()、@deprecated()等扩展装饰器的合并(见 controller-spec.ts#L71-L120)。
在服务端,rest.server.ts 的_setupOpenApiSpecEndpoints()会把规范端点挂到 Express 应用上。默认映射为(见 rest.server.ts#L1099-L1102):
{ "/openapi.json": {"version": "3.0.0", "format": "json"}, "/openapi.yaml": {"version": "3.0.0", "format": "yaml"} }同时/swagger-ui与/explorer路径会重定向到外部托管的 API Explorer(见 rest.server.ts#L336-L348)。如果需要定制端点,可通过OpenApiSpecOptions配置endpointMapping、setServersFromRequest、servers、disabled、consolidate等选项(见 rest.server.ts#L1104-L1142)。
查看你的 API 规范
要审查完整的 API 规范,把装饰过的控制器注册后运行应用,然后访问:
/openapi.json:以 JSON 格式获取规范;/openapi.yaml:以 YAML 格式获取规范。
也可以在代码中通过RestServer实例的getApiSpec()方法直接获取规范对象。从源码看(见 rest.server.ts#L835-L860),getApiSpec()会:
- 从绑定容器取基础规范
RestBindings.API_SPEC并深拷贝; - 合并 HTTP handler 中由路由装饰器解析出的
paths与components(深拷贝以避免调用方意外修改内部路由数据); - 若传入
requestContext,会根据请求上下文更新servers列表(例如启用setServersFromRequest时用请求的 base URL 替换 servers); - 最后依次应用已注册的 OAS enhancers(
OASEnhancer.applyAllEnhancers())后返回。
这一实现解释了文档所说的“规范由元数据构建、并提供路由逻辑”:同一个元数据源既用于生成/openapi.json展示的规范,也用于HttpHandler内部的 API 路由表。
对于自底向上方式开发一个完整应用的完整演练,可以阅读仓库中的 Todo 应用示例(examples/todo),它包含模型、数据源、仓储、控制器与序列(sequence)的完整代码。
小结
LoopBack 4 的 code-first 方式把“API 定义”收敛到 TypeScript 代码本身:@model()/@property()生成模型元数据并派生出 OpenAPI 组件 schema 与运行时校验;@operation()(及其@get/@post等简写)配合@param()/@requestBody()生成 paths 与操作对象;RestServer启动时把两者拼装为 OpenAPI 3.0 规范,同时驱动路由、参数解析与请求体校验,并通过/openapi.json、/openapi.yaml、getApiSpec()三种途径暴露给开发者审查。掌握这条“装饰器 → 元数据 → 规范 → 路由”的链路,就掌握了 LoopBack 4 code-first API 定义的核心机制。
- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
相关推荐
LoopBack 4 装饰器全景实践:OpenAPI 路由、@inject 依赖注入、@authenticate 认证与 Repository 模型装饰器
LoopBack 4 装饰器全景实践:OpenAPI 路由、@inject 依赖注入、@authenticate 认证与 Repository 模型装饰器 装饰
后端微服务从 LoopBack 4 的 OpenAPI 规范自动生成 Angular 前端应用
从 LoopBack 4 的 OpenAPI 规范自动生成 Angular 前端应用 导读 本文面向使用 LoopBack 4( loopback next 仓
后端微服务终极Kafka Connect实战指南:5步掌握企业级数据集成
终极Kafka Connect实战指南:5步掌握企业级数据集成 Kafka Connect是Apache Kafka生态系统中用于实现数据集成的核心工具,它提供
后端云原生镜像仓库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考