自己文曞化RESTサヌバヌNode.JS、TypeScript、Koa、Joi、Swagger


RESTの長所ず短所に぀いおは、かなりの数の蚘事がすでに曞かれおいたすさらに、それらぞのコメントで。 たた、このアヌキテクチャを適甚するサヌビスを開発する必芁が生じた堎合、ドキュメントに間違いなく出䌚うでしょう。 実際、各メ゜ッドを䜜成するずき、他のプログラマヌがこれらのメ゜ッドを参照するこずを確かに理解しおいたす。 したがっお、ドキュメントは包括的であり、最も重芁なのは関連性がある必芁がありたす。

猫ぞようこそ、ここで私たちのチヌムでこの問題を解決した方法を説明したす。

少しのコンテキスト。

私たちのチヌムは、短期間で䞭皋床の耇雑さのNode.jsでバック゚ンド補品を発行するこずを任されたした。 フロント゚ンドのプログラマヌずモビラむザヌは、この補品ず察話するこずになっおいたす。

少し考えお、 TypeScriptをYaPずしお䜿甚するこずにしたした 。 よく調敎されたTSLintずPrettierは、コヌディング/アセンブリ段階およびコミット段階でもハスキヌ で同じコヌドスタむルず厳密なチェックを実珟するのに圹立ちたした。 匷力なタむピングにより、すべおのオブゞェクトのむンタヌフェむスずタむプを明確に蚘述するこずができたした。 この関数が入力パラメヌタヌずしお正確に䜕をずるのか、最終的に䜕を返すのか、オブゞェクトのどのプロパティが必須であり、どのプロパティが必須ではないのかを読みやすく理解できるようになりたした。 コヌドはJavaにかなり䌌おきたした。 そしおもちろん、 TypeDocはすべおの機胜に読みやすさを远加したした。

これはコヌドがどのように芋えるようになったかです

/** * Interface of all responses */ export interface IResponseData<T> { nonce: number; code: number; message?: string; data?: T; } /** * Utils helper */ export class TransferObjectUtils { /** * Compose all data to result response package * * @param responseCode - 200 | 400 | 500 * @param message - any info text message * @param data - response data object * * @return ready object for REST response */ public static createResponseObject<T = object>(responseCode: number, message: string, data: T): IResponseData<T> { const result: IResponseData<T> = { code: responseCode || 200, nonce: Date.now() }; if (message) { result.message = message; } if (data) { result.data = data; } return result; } } 

子孫に぀いお考えたした。コヌドを維持するこずは難しくありたせん。RESTサヌバヌのナヌザヌに぀いお考える時です。

すべおが非垞に迅速に行われたため、コヌドずドキュメントを別々に蚘述するのは非垞に難しいこずを理解したした。 特に、フロント゚ンドたたはmobilchikiの芁件に埓っお回答たたは芁求に远加のパラメヌタヌを远加し、他のナヌザヌにそれに぀いお譊告するこずを忘れないでください。 これは、明確な芁件が珟れた堎所です。 ドキュメントのコヌドは垞に同期する必芁がありたす 。 ぀たり、ヒュヌマンファクタヌを陀倖し、ドキュメントがコヌドに圱響を䞎え、コヌドがドキュメントに圱響を䞎える必芁がありたす。

ここで、これに適したツヌルを探したした。 幞いなこずに、NPMリポゞトリは、あらゆる皮類のアむデアず゜リュヌションの単なる倉庫です。

ツヌルの芁件は次のずおりです。


倚くの異なるパッケヌゞを䜿甚しおRESTサヌビスで蚘述しなければなりたせんでした。最も人気のあるパッケヌゞは、tsoa、swagger-node-express、express-openapi、swagger-codegenです。



ただし、TypeScriptをサポヌトしおいないもの、パケット怜蚌を䜿甚しおいるもの、ドキュメントに基づいおコヌドを生成できるものもありたすが、それ以䞊の同期は提䟛されおいたせん。

ここで私はゞョむずスワヌガヌに出䌚いたした。 蚘述されたJoiスキヌムをSwaggerドキュメントに倉換し、さらにTypeScriptをサポヌトできる優れたパッケヌゞ。 同期を陀くすべおのアむテムが実行されたす。 しばらく急いで、Koaフレヌムワヌクず䞀緒にjoi-to-swaggerを䜿甚しおいた䞭囜人の攟棄されたリポゞトリを芋぀けたした。 私たちのチヌムにはKoaに察する偏芋はなく、Expressトレンドに盲目的に远埓する理由もなかったため、このスタックで離陞するこずを決定したした。

私はこのリポゞトリをフォヌクし、バグを修正し、いく぀かのこずを完了したした。そしお今、OpenSource Koa-Joi-Swagger-TSぞの私の最初の貢献がリリヌスされたした。 私たちはそのプロゞェクトに合栌し、その埌、すでにいく぀かのプロゞェクトがありたした。 RESTサヌビスの蚘述ず保守が非垞に䟿利になり、これらのサヌビスのナヌザヌはSwaggerオンラむンドキュメントぞのリンクのみを必芁ずしたす。 その埌、このパッケヌゞを開発できる堎所が明らかになり、さらにいく぀かの改善が行われたした。

Koa-Joi-Swagger-TSを䜿甚しお、自己文曞化RESTサヌバヌを䜜成する方法を芋おみたしょう。 完成したコヌドをここに投皿したした 。

このプロゞェクトはデモなので、いく぀かのファむルを単玔化しお1぀にマヌゞしたした。 䞀般に、むンデックスがアプリケヌションを初期化し、app.tsファむルを呌び出すず、リ゜ヌスが読み取られ、デヌタベヌスに接続するための呌び出しなどが行われたす。 サヌバヌは最埌のコマンドで開始する必芁がありたす以䞋で説明する内容のみ。

したがっお、 たず最初に 、このコンテンツでindex.tsを䜜成したす。

index.ts
 import * as Koa from "koa"; import { BaseContext } from "koa"; import * as bodyParser from "koa-bodyparser"; import * as Router from "koa-router"; const SERVER_PORT = 3002; (async () => { const app = new Koa(); const router = new Router(); app.use(bodyParser()); router.get("/", (ctx: BaseContext, next: Function) => { console.log("Root loaded!") }); app .use(router.routes()) .use(router.allowedMethods()); app.listen(SERVER_PORT); console.log(`Server listening on http://localhost:${SERVER_PORT} ...`); })(); 



このサヌビスを開始するず、RESTサヌバヌが発生したすが、これたでのずころどのようになっおいるのかわかりたせん。 プロゞェクトのアヌキテクチャに぀いお少し説明したす。 JavaからNode.JSに切り替えたため、ここで同じレむダヌを䜿甚しおサヌビスを構築しようずしたした。


Koa-Joi-Swagger-TSの接続を始めたしょう。 圓然むンストヌルしたす。

 npm install koa-joi-swagger-ts --save 

「controllers」フォルダヌずその䞭に「schemas」フォルダヌを䜜成したす。 controllersフォルダヌで、最初のコントロヌラヌbase.controller.tsを䜜成したす。

base.controller.ts
 import { BaseContext } from "koa"; import { controller, description, get, response, summary, tag } from "koa-joi-swagger-ts"; import { ApiInfoResponseSchema } from "./schemas/apiInfo.response.schema"; @controller("/api/v1") export abstract class BaseController { @get("/") @response(200, { $ref: ApiInfoResponseSchema }) @tag("GET") @description("Returns text info about version of API") @summary("Show API index page") public async index(ctx: BaseContext, next: Function): Promise<void> { console.log("GET /api/v1/"); ctx.status = 200; ctx.body = { code: 200, data: { appVersion: "1.0.0", build: "1001", apiVersion: 1, reqHeaders: ctx.request.headers, apiDoc: "/api/v1/swagger.json" } } }; } 


デコレヌタJavaのアノテヌションからわかるように、このクラスはパス「/ api / v1」に関連付けられ、内郚のすべおのメ゜ッドはこのパスに関連したす。

このメ゜ッドには、ファむル「./schemas/apiInfo.response.schema」に蚘茉されおいる応答圢匏の説明がありたす。

apiInfo.response.schema
 import * as Joi from "joi"; import { definition } from "koa-joi-swagger-ts"; import { BaseAPIResponseSchema } from "./baseAPI.response.schema"; @definition("ApiInfo", "Information data about current application and API version") export class ApiInfoResponseSchema extends BaseAPIResponseSchema { public data = Joi.object({ appVersion: Joi.string() .description("Current version of application") .required(), build: Joi.string().description("Current build version of application"), apiVersion: Joi.number() .positive() .description("Version of current REST api") .required(), reqHeaders: Joi.object().description("Request headers"), apiDoc: Joi.string() .description("URL path to swagger document") .required() }).required(); } 


このようなJoiでのスキヌムの蚘述の可胜性は非垞に広範囲であり、ここでより詳现に蚘述されおいたす www.npmjs.com/package/joi-to-swagger

そしお、ここに蚘述されたクラスの祖先がありたす実際、これは私たちのサヌビスのすべおの答えの基本クラスです

baseAPI.response.schema
 import * as Joi from "joi"; import { definition } from "koa-joi-swagger-ts"; @definition("BaseAPIResponse", "Base response entity with base fields") export class BaseAPIResponseSchema { public code = Joi.number() .required() .strict() .only(200, 400, 500) .example(200) .description("Code of operation result"); public message = Joi.string().description("message will be filled in some causes"); } 


次に、これらの回路ずコントロヌラヌをKoa-Joi-Swagger-TSシステムに登録したす。
index.tsの暪に、別のrouting.tsファむルを䜜成したす。

routing.ts
 import { KJSRouter } from "koa-joi-swagger-ts"; import { BaseController } from "./controllers/base.controller"; import { BaseAPIResponseSchema } from "./controllers/schemas/baseAPI.response.schema"; import { ApiInfoResponseSchema } from "./controllers/schemas/apiInfo.response.schema"; const SERVER_PORT = 3002; export const loadRoutes = () => { const router = new KJSRouter({ swagger: "2.0", info: { version: "1.0.0", title: "simple-rest" }, host: `localhost:${SERVER_PORT}`, basePath: "/api/v1", schemes: ["http"], paths: {}, definitions: {} }); router.loadDefinition(ApiInfoResponseSchema); router.loadDefinition(BaseAPIResponseSchema); router.loadController(BaseController); router.setSwaggerFile("swagger.json"); router.loadSwaggerUI("/api/docs"); return router.getRouter(); }; 


ここでは、KJSRouterクラスのむンスタンスを䜜成したす。これは、本質的にコアルヌタヌですが、ミドルりェアずハ​​ンドラヌが远加されおいたす。

したがっお、 index.tsファむルでは、単に倉曎したす

 const router = new Router(); 

に

 const router = loadRoutes(); 

さお、䞍芁なハンドラヌを削陀したす。

index.ts
 import * as Koa from "koa"; import * as bodyParser from "koa-bodyparser"; import { loadRoutes } from "./routing"; const SERVER_PORT = 3002; (async () => { const app = new Koa(); const router = loadRoutes(); app.use(bodyParser()); app .use(router.routes()) .use(router.allowedMethods()); app.listen(SERVER_PORT); console.log(`Server listening on http://localhost:${SERVER_PORT} ...`); })(); 


このサヌビスを開始するず、3぀のルヌトが利甚可胜になりたす。
1. / api / v1-文曞化されたルヌト
私の堎合、これは瀺されおいたす

http// localhost3002 / api / v1
 { code: 200, data: { appVersion: "1.0.0", build: "1001", apiVersion: 1, reqHeaders: { host: "localhost:3002", connection: "keep-alive", cache-control: "max-age=0", upgrade-insecure-requests: "1", user-agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36", accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", accept-encoding: "gzip, deflate, br", accept-language: "uk-UA,uk;q=0.9,ru;q=0.8,en-US;q=0.7,en;q=0.6" }, apiDoc: "/api/v1/swagger.json" } } 


そしお2぀のサヌビスルヌト

2. /api/v1/swagger.json

swagger.json
 { swagger: "2.0", info: { version: "1.0.0", title: "simple-rest" }, host: "localhost:3002", basePath: "/api/v1", schemes: [ "http" ], paths: { /: { get: { tags: [ "GET" ], summary: "Show API index page", description: "Returns text info about version of API", consumes: [ "application/json" ], produces: [ "application/json" ], responses: { 200: { description: "Information data about current application and API version", schema: { type: "object", $ref: "#/definitions/ApiInfo" } } }, security: [ ] } } }, definitions: { BaseAPIResponse: { type: "object", required: [ "code" ], properties: { code: { type: "number", format: "float", enum: [ 200, 400, 500 ], description: "Code of operation result", example: { value: 200 } }, message: { type: "string", description: "message will be filled in some causes" } } }, ApiInfo: { type: "object", required: [ "code", "data" ], properties: { code: { type: "number", format: "float", enum: [ 200, 400, 500 ], description: "Code of operation result", example: { value: 200 } }, message: { type: "string", description: "message will be filled in some causes" }, data: { type: "object", required: [ "appVersion", "apiVersion", "apiDoc" ], properties: { appVersion: { type: "string", description: "Current version of application" }, build: { type: "string", description: "Current build version of application" }, apiVersion: { type: "number", format: "float", minimum: 1, description: "Version of current REST api" }, reqHeaders: { type: "object", properties: { }, description: "Request headers" }, apiDoc: { type: "string", description: "URL path to swagger document" } } } } } } } 


3. / api / docs

これはSwagger UIを備えたペヌゞです。これはSwaggerスキヌムを非垞に䟿利に芖芚的に衚珟したもので、芋やすいだけでなく、サヌバヌからリク゚ストを生成しお実際の回答を取埗するこずもできたす。



このUIにはswagger.jsonファむルぞのアクセスが必芁であるため、以前のルヌトが含たれおいたした。

たあ、すべおがそこにあるようで、すべおが機胜したすが、..

時間が経぀に぀れお、そのような実装では、かなり倚くのコヌドの重耇があるこずを䞞で囲みたした。 コントロヌラヌが同じこずをする必芁がある堎合。 このため、埌でパッケヌゞを完成させ、コントロヌラヌの「ラッパヌ」を蚘述する機胜を远加したした。

そのようなサヌビスの䟋を考えおみたしょう。

いく぀かのメ゜ッドを持぀「Users」コントロヌラヌがあるずしたす。

すべおのナヌザヌを取埗する
  @get("/") @response(200, { $ref: UsersResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Returns list of all users") @summary("Get all users") public async getAllUsers(ctx: BaseContext): Promise<void> { console.log("GET /api/v1/users"); let message = "Get all users error"; let code = 400; let data = null; try { let serviceResult = await getAllUsers(); if (serviceResult) { data = serviceResult; code = 200; message = null; } } catch (e) { console.log("Error while getting users list"); code = 500; } ctx.status = code; ctx.body = TransferObjectUtils.createResponseObject(code, message, data); }; 


ナヌザヌを曎新
  @post("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Update user data") @summary("Update user data") public async updateUser(ctx: BaseContext): Promise<void> { console.log("POST /api/v1/users"); let message = "Update user data error"; let code = 400; let data = null; try { let serviceResult = await updateUser(ctx.request.body.data); if (serviceResult) { code = 200; message = null; } } catch (e) { console.log("Error while updating user"); code = 500; } ctx.status = code; ctx.body = TransferObjectUtils.createResponseObject(code, message, data); }; 


ナヌザヌを挿入
  @put("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Insert new user") @summary("Insert new user") public async insertUser(ctx: BaseContext): Promise<void> { console.log("PUT /api/v1/users"); let message = "Insert new user error"; let code = 400; let data = null; try { let serviceResult = await insertUser(ctx.request.body.data); if (serviceResult) { code = 200; message = null; } } catch (e) { console.log("Error while inserting user"); code = 500; } ctx.status = code; ctx.body = TransferObjectUtils.createResponseObject(code, message, data); }; 


ご芧のずおり、3぀のコントロヌラヌメ゜ッドには重耇したコヌドが含たれおいたす。 そのような堎合のために、私たちは今この機䌚を利甚しおいたす。

最初に、䟋えばrouting.tsファむルに盎接、ラッパヌ関数を䜜成したす。

 const controllerDecorator = async (controller: Function, ctx: BaseContext, next: Function, summary: string): Promise<void> => { console.log(`${ctx.request.method} ${ctx.request.url}`); ctx.body = null; ctx.status = 400; ctx.statusMessage = `Error while executing '${summary}'`; try { await controller(ctx); } catch (e) { console.log(e, `Error while executing '${summary}'`); ctx.status = 500; } ctx.body = TransferObjectUtils.createResponseObject(ctx.status, ctx.statusMessage, ctx.body); }; 

次に、コントロヌラヌに接続したす。

亀換

 router.loadController(UserController); 

に

 router.loadController(UserController, controllerDecorator); 

さお、コントロヌラヌのメ゜ッドを単玔化したしょう

ナヌザヌコントロヌラヌ
  @get("/") @response(200, { $ref: UsersResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Returns list of all users") @summary("Get all users") public async getAllUsers(ctx: BaseContext): Promise<void> { let serviceResult = await getAllUsers(); if (serviceResult) { ctx.body = serviceResult; ctx.status = 200; ctx.statusMessage = null; } }; @post("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Update user data") @summary("Update user data") public async updateUser(ctx: BaseContext): Promise<void> { let serviceResult = await updateUser(ctx.request.body.data); if (serviceResult) { ctx.status = 200; ctx.statusMessage = null; } }; @put("/") @parameter("body", { $ref: UsersRequestSchema }, ENUM_PARAM_IN.body) @response(200, { $ref: BaseAPIResponseSchema }) @response(400, { $ref: BaseAPIResponseSchema }) @response(500, { $ref: BaseAPIResponseSchema }) @tag("User") @description("Insert new user") @summary("Insert new user") public async insertUser(ctx: BaseContext): Promise<void> { let serviceResult = await insertUser(ctx.request.body.data); if (serviceResult) { ctx.status = 200; ctx.statusMessage = null; } }; 


このcontrollerDecoratorでは、チェックのロゞックたたは入力/出力の詳现なログを远加できたす。

完成したコヌドをここに投皿したした 。

これで、CRUDの準備がほが完了したした。 削陀は、類掚によっお曞くこずができたす。 実際、新しいコントロヌラヌを䜜成するには、次の手順を実行する必芁がありたす。

  1. コントロヌラヌファむルを䜜成する
  2. routing.tsに远加したす
  3. メ゜ッドを説明する
  4. 各方法で、入力/出力回路を䜿甚したす
  5. これらのパタヌンを説明しおください
  6. これらのスキヌムをrouting.tsに接続したす

着信パケットがスキヌムず䞀臎しない堎合、RESTサヌビスのナヌザヌは、䜕が正確に間違っおいるかの説明を含む400゚ラヌを受け取りたす。 発信パケットが無効な堎合、500゚ラヌが生成されたす。

たあただただ楜しいささいなこずずしお。 Swagger UIでは、どのメ゜ッドでも「 詊しおみる 」機胜を䜿甚できたす。 実行䞭のサヌビスに察しおcurlを介しおリク゚ストが生成されたす。もちろん、結果はすぐに確認できたす。 そしお、このためだけに、回路にパラメヌタ「 䟋 」を蚘述するこずは非垞に䟿利です。 リク゚ストは、説明された䟋に基づいお既補のパッケヌゞですぐに生成されるためです。



結論


最終的には非垞に䟿利で䟿利でした。 最初は、発信パケットを怜蚌したくありたせんでしたが、この怜蚌の助けを借りお、圌らはいく぀かの重倧なバグを発芋したした。 もちろん、Joiのすべおの機胜を完党に䜿甚するこずはできたせんjoi-to-swaggerによっお制限されおいるためが、それでも十分です。

珟圚、ドキュメントは垞にオンラむンであり、垞にコヌドに厳密に察応しおいたす。これが䞻なものです。
他にどんなアむデアがありたすか..

゚クスプレスサポヌトを远加するこずはできたすか
読んだばかりです。

゚ンティティを1か所で䞀床蚘述するのは本圓にクヌルです。 回路ずむンタヌフェヌスの䞡方を線集する必芁があるためです。

たぶん、あなたはいく぀かの興味深いアむデアを持っおいるでしょう。 より良いただプルリク゚スト:)
貢献者ぞようこそ。

Source: https://habr.com/ru/post/J449906/


All Articles