- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
Design-first(设计先行 / top-down)是 LoopBack 4 中"先定义 API 契约、再实现控制器"的构建方式:先用示例数据草拟 API 形态,抽象出可复用的 OpenAPI 片段模板,将各控制器的规格片段合并为一份完整的 OpenAPI Spec,最后通过@api装饰器与app.api(spec)绑定到RestApplication/RestServer,由框架按x-controller-name/x-operation-name扩展字段自动建立路由。读完后你将掌握 design-first 的完整工作流,并了解当前仓库源码中规格绑定、路由解析与规格校验的实际实现位置及适用限制。
重要前提:本文基于仓库中的 Defining the API using design-first approach。该文档原文带有显著提示:"The top-down approach for building LoopBack applications is not yet fully supported. Therefore, the steps outlined in this page are outdated and may not work out of the box."(top-down 方式尚未被完整支持,文档中的步骤可能无法开箱即用。)因此在当前仓库中,design-first 更适合作为API 契约规划与文档产出手段,运行时路由的主力仍是 code-first 方式(见 Defining the API using code-first approach)。下文所有代码沿用原文档示例,并标注了与当前仓库源码的对应关系和必要适配点。
从示例数据出发:草拟 API 的形态
构建 API 时,通常最容易入手的第一步是列出消费者需要的示例数据。它可以直接充当小型应用 API 规格的初稿。原文档以一组商品数据为例:
const products = [ { name: 'Headphones', price: 29.99, category: '/categories/accessories', available: true, deals: ['50% off', 'free shipping'], }, { name: 'Mouse', price: 32.99, category: '/categories/accessories', available: true, deals: ['30% off', 'free shipping'], }, { name: 'yPhone', price: 299.99, category: '/categories/phones', available: true, deals: ['free shipping'], }, { name: 'yBook', price: 5999.99, category: '/categories/computers', available: true, }, ];定义了示例数据后,就可以把数据拆分成一组"专有名词"(proper nouns),它们后续会以 resource、schema、model 或 repository 等形式落地:
CatalogItem—— 上述数组中的每个对象;Category—— 拥有 URL,并带有更丰富的类目信息;Product—— 名称、价格等商品属性;Inventory—— 商品库存/可得性;Deals—— 针对一组商品的促销信息。
这一步不写任何框架代码,纯粹是领域建模,目的是先锁定 API 要暴露哪些概念。
勾勒 URL 结构并拆分为 Resources
专有名词确定后,需要决定 API 的粒度(fine-grained 还是 coarse-grained):哪些名词会暴露为独立的Resource。原文档给出的判断方法是"先把 URL(不带动词)画出来":
/products?{query}—— 在目录中搜索商品;/product/{slug}—— 获取某个商品的详情;/deals?{query}—— 搜索促销;/deal/{slug}—— 获取某个促销的详情;/categories?{query}—— 搜索类目;/category/{slug}—— 获取某个类目的详情;/category/{slug}/products?{query}—— 搜索某个类目下的商品。
由 URL 可以直接得出三个 Resource:ProductResource、DealResource、CategoryResource。
进一步观察会发现它们存在明显的共性:除CategoryResource多出的/category/{slug}/products?{query}之外,三者的 URL 结构完全一致,可以抽象为两个模式:
/{pluralName}?{query}—— 按复数名搜索(带 query);/{name}/{slug}—— 获取资源详情。
识别出这种共性,正是下一步"用模式减少重复"的基础。
用模板函数复用 OpenAPI 片段
模式(pattern)可能在未来被调整,为了保持灵活性,原文档建议用简单的 TypeScript 函数(JavaScript 亦可)来定义模式。
SearchableResource:搜索与详情模式
所有资源都需要支持相同的搜索与列表操作,因此先从SearchableResource模式入手。它定义了支持搜索的 OpenAPI 片段的全部语义(对应/{pluralName}?{query}与/{name}/{slug}两个模式):
// apidefs/templates/searchable-resource.ts export let searchableResource = (resource: any, type: string) => ({ paths: { [`/${resource.path}`]: { // pattern get: { parameters: [ { in: 'query', name: 'filter', type: 'string', }, ], responses: { 200: { description: resource.description || `Result set of type ${type} returned.`, schema: { $ref: `#/definitions/${type}`, type: 'array', }, }, }, 'x-controller-name': resource.controller, 'x-operation-name': 'search', }, }, [`/${resource.path}/{slug}`]: { // pattern get: { parameters: [ { in: 'path', name: 'slug', required: true, type: 'string', }, ], responses: { 200: { description: resource.description || `Result of type ${type} returned.`, schema: { $ref: `#/definitions/${type}`, }, }, }, 'x-controller-name': resource.controller, 'x-operation-name': 'getDetails', }, }, }, });这里两个x-扩展字段是整个 design-first 流程的关键枢纽:
x-controller-name指向实现该操作的控制器类名;x-operation-name指向控制器中的方法名。
当前仓库源码完整印证了这一机制。RestServer在创建 HTTP 处理器时会读取绑定的完整规格,并对每个 path/verb 调用_setupOperation(见 rest.server.ts):
// packages/rest/src/rest.server.ts(节选,L491-L539 附近) private _setupOperation(verb: string, path: string, spec: OperationObject) { const handler = spec['x-operation']; if (typeof handler === 'function') { // 直接以函数为 handler 的路由 // ... return; } const controllerName = spec['x-controller-name']; if (typeof controllerName === 'string') { const b = this.getBinding(`controllers.${controllerName}`, { optional: true, }); if (!b) { throw new Error( `Unknown controller ${controllerName} used by "${verb} ${path}"`, ); } // ... 构造 ControllerRoute 并注册 return; } throw new Error( `There is no handler configured for operation "${verb} ${path}"`, ); }从源码结构看:如果某个 operation 既没有x-controller-name(或x-operation函数),服务启动注册路由时会直接抛出There is no handler configured for operation "..."错误。因此design-first 模式下每个片段都必须正确填写这两个扩展字段,这是模板函数把resource.controller透传到'x-controller-name'的原因。
OpenAPI 2/3 适配提示:示例中
responses.200.schema直接内嵌type/$ref的写法偏 OpenAPI 2 风格。按 OpenAPI 3 规范应写作content: {'application/json': {schema: {...}}};#/definitions/...也应改为#/components/schemas/...。当前仓库的RestServer.getApiSpec()会合并控制器元数据生成的components(见 rest.server.ts),因此使用 OpenAPI 3 风格是更稳妥的选择。
CreatableResource:创建模式
同理,为POST定义一个CreatableResource模板:
// apidefs/templates/creatable-resource.ts export let creatableResource = (resource: any, type: string) => ({ paths: { [`/${resource.path}`]: { // pattern post: { parameters: [ { in: 'body', name: 'body', required: true, schema: { $ref: `#/definitions/${type}`, }, }, ], responses: { 201: { description: resource.description || `The ${type} instance was created.`, schema: { $ref: `#/definitions/${type}`, }, }, }, 'x-controller-name': resource.controller, 'x-operation-name': 'create', }, }, }, });OpenAPI 3 适配提示:OpenAPI 3 中没有
in: 'body'参数,请求体应改用requestBody: {required: true, content: {...}}表达。
TypeDefinition:类型定义辅助函数
最后一个辅助函数用于生成 OpenAPI 中的类型定义:
// apidefs/templates/type-definition.ts import {DefinitionsObject} from '@loopback/openapi-spec'; export let TypeDefinition = (type: any): DefinitionsObject => ({ definitions: { [`${type.name}`]: { properties: type.definition, }, }, });需要注意:示例中引用的@loopback/openapi-spec包在当前仓库的 packages 目录中已不存在(当前与 OpenAPI 相关的包是 openapi-v3 与 openapi-spec-builder)。按原文档时代,DefinitionsObject属于 OpenAPI 2 的概念;迁移到当前代码基线时,应改用@loopback/openapi-v3中的 OpenAPI 3 类型(如SchemaObject、ComponentsObject),并把definitions改为components.schemas。
组装 Product 控制器的 API 片段
有了模式函数,就可以生成代表ProductController那份 OpenAPI 片段。原文档使用 lodash 帮助合并各生成物(原文档给出的安装命令为npm install --save lodash;在当前仓库实践中也可使用 TypeScript 内置的lodash.merge等价写法):
// apidefs/product.api.ts import _ from 'lodash'; // Assuming you have created the "base" schema elsewhere. // If there are no common properties between all of the endpoint objects, // then you can ignore this. import BaseSchema from '../BaseSchema'; // Don't forget to export the template functions under a common file! import { SearchableResource, CreatableResource, TypeDefinition, } from './templates'; let ProductAPI: ControllerSpec = {}; const ProductDefinition = {}; // Build type definition using base schema + additional properties. _.merge( ProductDefinition, BaseSchema, TypeDefinition({ price: { type: 'number', minimum: 0, exclusiveMinimum: true, }, }), ); const ProductGetResource = SearchableResource( { controller: 'ProductController', operation: 'search', path: 'products', }, 'Product', ); const ProductCreateResource = CreatableResource( { controller: 'ProductController', operation: 'create', path: 'products', }, 'Product', ); // Rinse and repeat for PUT, PATCH, DELETE, etc... // Merge all of the objects together. // This will mix the product definition into the "definitions" property of the // OpenAPI spec, and the resources will be mixed into the "paths" property. _.merge( ProductAPI, ProductDefinition, ProductGetResource, ProductCreateResource, ); // And export it! export default ProductAPI;片段组装的产物结构,与当前仓库中ControllerSpec接口的定义是同一形状——见 controller-spec.ts:
// packages/openapi-v3/src/controller-spec.ts export interface ControllerSpec { basePath?: string; // API 服务的基础路径,需以 / 开头 paths: PathObject; // 路径与操作 components?: ComponentsObject; // 由模型元数据生成的 schemas 等 }也就是说,每个控制器片段本质上就是一个Partial<ControllerSpec>:paths承载操作,components(或旧版的definitions)承载类型定义。这样拆分的好处是每个"模型级" API 导出的边界清晰,之后可以与对应的控制器一一绑定。
如果不想手写_.merge,当前仓库提供了类型化的规格构建器 OpenApiSpecBuilder(包@loopback/openapi-spec-builder),其构造器默认就生成openapi: '3.0.0'、info、paths: {}、servers: [{url: '/'}]的骨架,并通过withOperation(verb, path, spec)、withControllerName()、withOperationName()等方法(见 openapi-spec-builder.ts)显式写入x-controller-name/x-operation-name,可作为 lodash 手工合并的更类型安全的替代方案。
将 API 片段绑定到控制器
按"每个控制器一个 API 片段"的方式拆分后,就用@api类装饰器把片段绑定到控制器,从而建立到具体方法的映射:
// controllers/product-controller.ts import {api} from '@loopback/core'; // 当前仓库中应改为 from '@loopback/openapi-v3' import ProductApi from '../apidefs/product.api'; // This decorator binds the Product API to the controller, // which will establish routing to the specified functions below. @api(ProductApi) export class ProductController { // Note that the function names here match the strings in the "operation" // property you provided to the SearchableResource call in the previous // example. public search() { // your logic here } // Same goes for this function! public create(id: number, name: string, price: number) { // your logic here } // etc... }导入来源提示:原文档写的是
import {api} from '@loopback/core',但当前仓库中@api装饰器的实现位于 packages/openapi-v3/src/decorators/api.decorator.ts,应从@loopback/openapi-v3导入。
@api装饰器在当前仓库中的实现非常薄:
// packages/openapi-v3/src/decorators/api.decorator.ts export function api(spec: Partial<ControllerSpec>) { const controllerSpec: ControllerSpec = {paths: {}, ...spec}; return ClassDecoratorFactory.createDecorator<ControllerSpec>( OAI3Keys.CLASS_KEY, controllerSpec, {decoratorName: '@api'}, ); }它只是把规格以ControllerSpec形式写入类的元数据(键定义在 keys.ts 的OAI3Keys.CLASS_KEY,即openapi-v3:class)。
真正消费这份元数据的是resolveControllerSpec/getControllerSpec(见 controller-spec.ts):它会读取@api写入的类级规格并深拷贝,随后逐方法处理装饰器元数据,最终自动补齐三个关键字段:
// packages/openapi-v3/src/controller-spec.ts(L356-L368 附近) operationSpec['x-operation-name'] = op; operationSpec['x-controller-name'] = operationSpec['x-controller-name'] || constructor.name; if (operationSpec.operationId == null) { // Build the operationId as `<controllerName>.<operationName>` operationSpec.operationId = operationSpec['x-controller-name'] + '.' + operationSpec['x-operation-name']; }这说明x-controller-name/x-operation-name是 LoopBack 内部路由协议:RestServer依赖它们把paths中的 operation 映射回控制器实例方法(见前文_setupOperation),而operationId则默认按<controllerName>.<operationName>生成。因此模板函数里'x-operation-name': 'search'与控制器方法search()的命名一致,不是巧合而是硬性要求。
组装最终规格并绑定到应用
各控制器的 OpenAPI 片段就绪后,合并出最终规格(原文档文件名为apidefs/swagger.ts):
// apidefs/swagger.ts import {ProductAPI, DealAPI, CategoryAPI} from '../apidefs'; import OpenApiSpec from '@loopback/openapi-spec'; // 当前仓库中为 '@loopback/openapi-v3' import _ from 'lodash'; // Import API fragments here export const spec: OpenApiSpec = { openapi: '3.0.0', info: { title: 'Your API', version: '1.0', }, paths: {}, servers: [{url: '/'}], }; _.merge(spec, ProductAPI); _.merge(spec, DealAPI); _.merge(spec, CategoryAPI); export default spec;适配提示:
OpenApiSpec类型在当前仓库由 openapi-v3 提供;上例骨架与 OpenApiSpecBuilder 构造器 的默认值(openapi: '3.0.0'、info、paths: {}、servers: [{url: '/'}])完全一致,可以直接对照。
绑定到应用
把完整规格绑定到应用使用app.api()。对只有一个 REST server 的应用,这是最自然的写法;如果是多 REST server、各提供不同 API 的应用,则应在 server 层调用(原文档表述为server.api())。原文档给出的示例:
// application.ts // This should be the export from the previous example. import spec from '../apidefs/swagger'; import {RestApplication, RestServer} from '@loopback/rest'; import { ProductController, DealController, CategoryController, } from './controllers'; export class YourMicroservice extends RestApplication { constructor() { super({ rest: { port: 3001, }, }); const app = this; app.controller(ProductController); app.controller(DealController); app.controller(CategoryController); //inject your spec app.api(spec); } // etc... }当前仓库源码证实了app.api()的机制(rest.application.ts):
// packages/rest/src/rest.application.ts api(spec: OpenApiSpec): Binding { return this.restServer.bind(RestBindings.API_SPEC).to(spec); }即把规格绑定到 RestBindings.API_SPEC(rest.apiSpec)。之后RestServer在_createHttpHandler中从该绑定读取规格,遍历spec.paths逐操作调用_setupOperation注册路由;若规格里带了components,还会先通过registerApiComponents注册组件(schema、requestBody 等),供后续校验与响应处理复用。
两点实践注意:
- 控制器必须注册:
app.controller(ProductController)使控制器以controllers.ProductController的键被绑定,_setupOperation正是通过this.getBinding('controllers.' + controllerName)找到它,未绑定会抛出Unknown controller ... used by "GET /products"这类错误。 - 控制器绑定在 app 层而非 server 层:原文档特意指出这一点——因为一个控制器可能被多个 server 实例(甚至不同类型)复用,而规格绑定则可以按 server 区分。
校验 API 规格
OpenAPI Swagger editor 一类的在线编辑器自带校验器,适合手动核对;但手动校验繁琐且易错。更好的做法是把校验放进 CI/CD 的自动化测试中。LoopBack 的testlab模块提供了validateApiSpec辅助函数,只需在测试套件里加一个 Mocha 用例:
// src/__tests__/acceptance/api-spec.acceptance.ts import {validateApiSpec} from '@loopback/testlab'; import {MyApp} from '../..'; import {RestServer} from '@loopback/rest'; describe('API specification', () => { it('api spec is valid', async () => { const app = new MyApp(); const server = await app.getServer(RestServer); const spec = await server.getApiSpec(); await validateApiSpec(spec); }); });(原文档示例中最后一行误写为validateApiSpec(apiSpec),此处按当前仓库 Testing your application 中"Validate your OpenAPI specification"一节的写法修正为对getApiSpec()返回值的校验;另外getApiSpec()在当前实现中是async方法,需要await。)
源码层面,validateApiSpec的实现见 packages/testlab/src/validate-api-spec.ts:
const validator = require('oas-validator'); import {promisify} from 'util'; const validateAsync = promisify(validator.validate); export async function validateApiSpec(spec: any): Promise<void> { await validateAsync(spec, {}); }从当前源码看,它对oas-validator的validate做了promisify包装,校验失败时会以 rejected promise 的形式让 Mocha 用例失败。配合RestServer.getApiSpec()使用尤其有意义——该方法返回的并不是你传入的原始对象,而是注册路由后的实际规格:它在 rest.server.ts 中把RestBindings.API_SPEC与 HTTP handler 中登记的paths、components做深拷贝合并,再经过 OAS enhancers 处理后返回。因此该测试实际校验的是"应用真实对外暴露的 API",能发现合并冲突、缺失 handler、schema 不完整等问题,而不只是你手工拼出来的静态 JSON。
@loopback/rest自身的集成测试也大量使用这一组合来验证导出的规格(见 rest.server.open-api-spec.unit.ts 与 export-openapi-spec.integration.ts),可作为编写自身校验用例的参考。
适用边界与当前仓库的适配要点
把原文档的 design-first 流程放到当前仓库代码基线上落地时,建议关注以下几点(均有源码或文档依据):
- 功能支持度:design-first(top-down)在原文档中即被标注为"尚未完整支持、步骤可能无法开箱即用",当前仓库的 Testing your application 中对自动 smoke test 的说明同样引用了这一限制。日常开发建议以 code-first 方式 定义 API,design-first 用于前期契约设计、规格文档化与 CI 校验。
- 包与类型迁移:原文档引用的
@loopback/openapi-spec、DefinitionsObject在当前仓库中已不存在;OpenAPI 3 的类型与@api、getControllerSpec等位于 openapi-v3 包,规格构建器位于 openapi-spec-builder 包。@api应从@loopback/openapi-v3导入。 - OpenAPI 2 → 3 语义差异:模板中的
#/definitions/...应改为#/components/schemas/...,in: 'body'参数应改为requestBody,响应 schema 应放入content['application/json'].schema。 x-controller-name/x-operation-name是硬契约:RestServer._setupOperation依赖这两个字段解析 handler,缺失会直接抛错;方法名必须与x-operation-name一致。- 校验自动化:用
@loopback/testlab的validateApiSpec+server.getApiSpec()写 Mocha 用例,纳入 CI,避免人工核对。
小结
design-first 的核心链路可以概括为:示例数据 → 领域名词 → URL 模式 → 模板函数(searchable/creatable/type)→ 控制器级 API 片段(Partial<ControllerSpec>)→ 合并为完整OpenApiSpec→@api绑定片段到控制器、app.api(spec)绑定完整规格 →validateApiSpec纳入 CI 校验。仓库源码中 api.decorator.ts、controller-spec.ts、rest.server.ts 与 rest.application.ts 分别对应了这条链路上的元数据写入、规格解析、路由注册与规格绑定四个环节,可作为深入阅读 design-first 实现细节的入口。
- 后端
- 微服务
【免费下载链接】loopback-next
LoopBack makes it easy to build modern API applications that require complex integrations.
相关推荐
一行命令装好、10 行代码出声:VoxCPM2 零样本语音克隆上手笔记
一行命令装好、10 行代码出声:VoxCPM2 零样本语音克隆上手笔记 给短视频配旁白,还要花钱请配音师?VoxCPM2 把文本转语音(TTS)和零样本语音克隆
人工智能语音音频AI 应用OpenBMB从 LoopBack 4 的 OpenAPI 规范自动生成 Angular 前端应用
从 LoopBack 4 的 OpenAPI 规范自动生成 Angular 前端应用 导读 本文面向使用 LoopBack 4( loopback next 仓
后端微服务LoopBack 4 应用配置实战:构造函数、手动绑定与自动绑定机制详解
LoopBack 4 应用配置实战:构造函数、手动绑定与自动绑定机制详解 LoopBack 4 的应用配置是搭建 API 服务的第一步:无论是控制端口、注册 C
后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考