REST API仕様が倧きな圹割を果たす5 + 1の堎合


この蚘事では、REST APIプロゞェクトの有甚で関連する仕様の䜜成ずサポヌトに焊点を圓おたす。これにより、倚くの䜙分なコヌドが節玄され、プロゞェクト党䜓の敎合性、信頌性、透明性が倧幅に向䞊したす。


RESTful APIずは䜕ですか



これは神話です。


真剣に、あなたのプロゞェクトがRESTful APIを持っおいるず思うなら、間違いなく間違いです。 RESTfulの考え方は、RESTスタむルで蚘述されたアヌキテクチャのルヌルず制限にすべおの点で準拠するAPIを構築するこずですが、実際の状況ではこれはほずんど䞍可胜です。


䞀方で、RESTには曖昧で曖昧な定矩が倚すぎたす。 たずえば、HTTPメ゜ッドおよびステヌタスコヌドのディクショナリからの䞀郚の甚語は、実際には意図された目的に䜿甚されず、それらの倚くはたったく䜿甚されたせん。


䞀方、RESTは非垞に倚くの制限を䜜成したす。 たずえば、珟実䞖界でのリ゜ヌスのアトミックな䜿甚は、モバむルアプリケヌションで䜿甚されるAPIにずっお合理的ではありたせん。 リク゚スト間で状態を保存するこずを完党に拒吊するこずは、倚くのAPIで䜿甚されるナヌザヌセッションメカニズムの犁止です。


しかし、すべおがそれほど悪いわけではありたせん


なぜREST API仕様が必芁なのですか


これらの欠点にも関わらず、合理的なアプロヌチで、RESTは䟝然ずしお本圓にクヌルなAPIを蚭蚈するための優れた基盀のたたです。 このようなAPIには、内郚の均䞀性、明確な構造、䟿利なドキュメント、および優れた単䜓テストのカバレッゞが必芁です。 これらはすべお、APIの品質仕様を開発するこずで実珟できたす。


ほずんどの堎合、REST API 仕様はそのドキュメントに関連付けられおいたす 。 最初のAPIAPIの正匏な説明ずは異なり、ドキュメントは人々が読むこずを目的ずしおいたす。たずえば、APIを䜿甚するモバむルたたはWebアプリケヌションの開発者です。


ただし、実際にドキュメントを䜜成するこずに加えお、適切なAPIの説明には倚くの利点がありたす。 仕様の有胜な䜿甚を䜿甚しお、次のこずができる方法の䟋を共有したい蚘事で



Openapi



珟圚REST APIを蚘述するために䞀般的に受け入れられおいる圢匏はOpenAPIであり、これはSwaggerずしおも知られおいたす。 この仕様は、JSONたたはYAML圢匏の単䞀ファむルであり、3぀のセクションで構成されおいたす。



OpenAPIには重倧な欠点がありたす- 構造の耇雑さず、倚くの堎合、冗長性 。 小さなプロゞェクトの堎合、仕様JSONファむルの内容はすぐに数千行に達する可胜性がありたす。 この圢匏でこのファむルを手動で管理するこずはできたせん。 これは、APIの進化に合わせお最新の仕様を維持するずいう考えに察する深刻な脅嚁です。


APIを蚘述し、結果のOpenAPI仕様を䜜成できるビゞュアル゚ディタヌが倚数ありたす。 同様に、远加のサヌビスずクラりド゜リュヌションは、たずえばSwagger 、 Apiary 、 Stoplight 、 Restletなどに基づいおいたす。


しかし、私にずっおは、仕様をすばやく線集しおコヌド蚘述プロセスず組み合わせるこずが難しいため、このようなサヌビスはあたり䟿利ではありたせんでした。 もう1぀のマむナス点は、特定の各サヌビスの機胜セットぞの䟝存です。 たずえば、クラりドサヌビスだけで本栌的な単䜓テストを実装するこずはほずんど䞍可胜です。 ゚ンドポむント甚のコヌド生成、さらには「プラグ」の䜜成は、非垞に可胜性があるように芋えたすが、実際には実際には圹に立ちたせん。


Tinyspec


この蚘事では、ネむティブREST API蚘述圢匏-tinyspecに基づく䟋を䜿甚したす。 圢匏は、プロゞェクトで䜿甚される゚ンドポむントずデヌタモデルを盎感的な構文で蚘述する小さなファむルです。 ファむルはコヌドの隣に保存されたす。これにより、ファむルの䜜成プロセスでファむルを確認しお線集するこずができたす。 同時に、tinyspecは、プロゞェクトですぐに䜿甚できる本栌的なOpenAPIに自動的にコンパむルされたす。 正確な方法を説明する時が来たした。


この蚘事では、Node.jskoa、expressずRuby on Railsの䟋を瀺したすが、これらのプラクティスはPython、PHP、Javaなどのほずんどのテクノロゞヌに適甚されたす。


仕様が非垞に圹立぀堎合


1.゚ンドポむントの単䜓テスト


行動駆動開発BDDは 、REST APIの開発に最適です。 単䜓テストを蚘述する最も䟿利な方法は、個々のクラス、モデル、コントロヌラヌではなく、特定の゚ンドポむント甚です。 各テストでは、実際のHTTP芁求を゚ミュレヌトし、サヌバヌの応答を確認したす。 Node.jsには、テストリク゚ストを゚ミュレヌトするためのスヌパヌテストずchai-httpがあり、Ruby on Railsに搭茉されおいたす。


Userスキヌマず、すべおのナヌザヌを返すGET /users゚ンドポむントがあるずしたす。 これを説明するtinyspec構文は次のずおりです。


  1. User.models.tinyspecファむル

 User {name, isAdmin: b, age?: i} 

  1. users.endpoints.tinyspecファむル

 GET /users => {users: User[]} 

これがテストの倖芳です。


Node.js


 describe('/users', () => { it('List all users', async () => { const { status, body: { users } } = request.get('/users'); expect(status).to.equal(200); expect(users[0].name).to.be('string'); expect(users[0].isAdmin).to.be('boolean'); expect(users[0].age).to.be.oneOf(['boolean', null]); }); }); 

Ruby on Rails


 describe 'GET /users' do it 'List all users' do get '/users' expect_status(200) expect_json_types('users.*', { name: :string, isAdmin: :boolean, age: :integer_or_null, }) end end 

サヌバヌの応答圢匏を蚘述する仕様がある堎合、テストを単玔化し、 この仕様に察しお応答をチェックするだけです。 これを行うには、tinyspecモデルがOpenAPI定矩に倉換されるずいう事実を利甚したす。OpenAPI定矩はJSONスキヌマ圢匏に察応したす。


JSのリテラルオブゞェクト たたはRubyのHash 、Pythonのdict 、PHPの連想配列 、さらにはJavaのMap は、JSONスキヌムぞの準拠をテストできたす。 たた、フレヌムワヌクをテストするための適切なプラグむンもありたす。たずえば、 RSpecのjest-ajv npm、 chai-ajv-json-schema npm、 json_matchers rubygemです。


スキヌムを䜿甚する前に、それらをプロゞェクトに接続する必芁がありたす。 最初に、tinyspecに基づいおopenapi.json仕様ファむルを生成したすこのアクションは、各テストの実行前に自動的に実行できたす。


 tinyspec -j -o openapi.json 

Node.js


これで、受信したJSONをプロゞェクトで䜿甚しお、そこからdefinitionsキヌを取埗できたす。これには、すべおのJSONスキヌムが含たれおいたす。 スキヌムには盞互参照 $ref を含めるこずができたす。したがっお、ネストされたスキヌムたずえば、 Blog {posts: Post[]} がある堎合、怜蚌で䜿甚するためにそれらを「展開」する必芁がありたす。 このために、 json-schema-deref-sync npmを䜿甚したす。


 import deref from 'json-schema-deref-sync'; const spec = require('./openapi.json'); const schemas = deref(spec).definitions; describe('/users', () => { it('List all users', async () => { const { status, body: { users } } = request.get('/users'); expect(status).to.equal(200); // Chai expect(users[0]).to.be.validWithSchema(schemas.User); // Jest expect(users[0]).toMatchSchema(schemas.User); }); }); 

Ruby on Rails


json_matchersは$refリンクを凊理できたすが、特定の方法でファむルシステムにスキヌムを持぀個別のファむルが必芁なので、最初にswagger.jsonを倚くの小さなファむルに「分割」するswagger.jsonがありたす詳现はこちら 。


 # ./spec/support/json_schemas.rb require 'json' require 'json_matchers/rspec' JsonMatchers.schema_root = 'spec/schemas' # Fix for json_matchers single-file restriction file = File.read 'spec/schemas/openapi.json' swagger = JSON.parse(file, symbolize_names: true) swagger[:definitions].keys.each do |key| File.open("spec/schemas/#{key}.json", 'w') do |f| f.write(JSON.pretty_generate({ '$ref': "swagger.json#/definitions/#{key}" })) end end 

その埌、次のようにテストを蚘述できたす。


 describe 'GET /users' do it 'List all users' do get '/users' expect_status(200) expect(result[:users][0]).to match_json_schema('User') end end 

泚この方法でテストを曞くこずは非垞に䟿利です。 特に、IDEがテストの実行ずデバッグWebStorm、RubyMine、Visual Studioなどをサポヌトしおいる堎合。 したがっお、他の゜フトりェアはたったく䜿甚できず、API開発サむクル党䜓が3぀の連続したステップに短瞮されたす。


  1. 仕様蚭蚈䟋tinyspec;
  2. 远加/倉曎された゚ンドポむントのテストの完党なセットを䜜成したす。
  3. すべおのテストを満たすコヌドの開発。

2.入力の怜蚌


OpenAPIは、応答だけでなく入力デヌタの圢匏も蚘述したす。 これにより、リク゚スト䞭にナヌザヌから受信したデヌタを怜蚌できたす。


ナヌザヌデヌタの曎新ず、倉曎可胜なすべおのフィヌルドを説明する次の仕様があるずしたす。


 # user.models.tinyspec UserUpdate !{name?, age?: i} # users.endpoints.tinyspec PATCH /users/:id {user: UserUpdate} => {success: b} 

以前、テスト内の怜蚌甚のプラグむンを怜蚎したしたが、より䞀般的な堎合にはajv npmおよびjson-schema rubygem怜蚌モゞュヌルがありたす。それらを䜿甚しお怜蚌付きのコントロヌラヌを䜜成したしょう。


Node.jsKoa


これは、Expressの埌継であるKoaの䟋ですが、Expressの堎合、コヌドは同様になりたす。


 import Router from 'koa-router'; import Ajv from 'ajv'; import { schemas } from './schemas'; const router = new Router(); // Standard resource update action in Koa. router.patch('/:id', async (ctx) => { const updateData = ctx.body.user; // Validation using JSON schema from API specification. await validate(schemas.UserUpdate, updateData); const user = await User.findById(ctx.params.id); await user.update(updateData); ctx.body = { success: true }; }); async function validate(schema, data) { const ajv = new Ajv(); if (!ajv.validate(schema, data)) { const err = new Error(); err.errors = ajv.errors; throw err; } } 

この䟋では、入力デヌタが仕様を満たしおいない堎合、サヌバヌはクラむアントに500 Internal Server Error応答を返したす。 これを防ぐために、バリデヌタ゚ラヌをむンタヌセプトし、独自の回答を䜜成できたす。これには、テストに合栌しなかった特定のフィヌルドに関する詳现情報が含たれ、仕様にも準拠したす。


FieldsValidationErrorファむルにFieldsValidationErrorモデルの説明を远加したす。


 Error {error: b, message} InvalidField {name, message} FieldsValidationError < Error {fields: InvalidField[]} 

そしお今、私たちはそれを゚ンドポむントの可胜な答えの1぀ずしお瀺したす


 PATCH /users/:id {user: UserUpdate} => 200 {success: b} => 422 FieldsValidationError 

このアプロヌチにより、クラむアントから受信した䞍正なデヌタを䜿甚しお゚ラヌの圢成が正しいこずを怜蚌する単䜓テストを䜜成できたす。


3.モデルのシリアル化


ほずんどすべおの最新のサヌバヌフレヌムワヌクは、 䜕らかの方法でORMを䜿甚したす。 ぀たり、システム内のAPIで䜿甚されるほずんどのリ゜ヌスは、モデル、そのむンスタンス、およびコレクションの圢匏で提瀺されたす。


API応答で送信するためにこれらの゚ンティティのJSON衚珟を生成するプロセスは、 シリアラむれヌションず呌ばれたす。 たずえば、 sequelize-to-json npm、 acts_as_api rubygem、 jsonapi-rails rubygemなど、シリアル化機胜を実行するさたざたなフレヌムワヌク甚のプラグむンがいく぀かありたす。 実際、これらのプラグむンを䜿甚するず、特定のモデルでJSONオブゞェクトに含める必芁があるフィヌルドのリストを指定したり、远加のルヌル名前の倉曎や倀の動的な蚈算などを指定したりできたす。


困難は、同じモデルの耇数の異なるJSON衚珟が必芁な堎合、たたはオブゞェクトにネストされた゚ンティティ関連付けが含たれる堎合に始たりたす。 シリアラむザヌの継承、再利甚、リンクが必芁です。


さたざたなモゞュヌルがこれらの問題をさたざたな方法で解決したすが、仕様が再び圹立぀かどうか考えおみたしょう。 実際、JSON衚珟の芁件に関するすべおの情報、ネストされた゚ンティティを含むフィヌルドの可胜なすべおの組み合わせは、すでにそこにありたす。 したがっお、自動シリアラむザヌを䜜成できたす。


小さなモゞュヌルsequelize-serialize npmに泚目しおください。これにより、Sequelizeモデルでこれを行うこずができたす。 モデルむンスタンスたたは配列、および必芁な回路の入力を受け取り、すべおの必芁なフィヌルドを考慮し、関連する゚ンティティにネストされた回路を䜿甚しお、シリアル化されたオブゞェクトを繰り返し構築したす。


そのため、ブログの投皿コメントぞのコメントを含むを持っおいるすべおのナヌザヌをAPIから返す必芁があるずしたす。 これに぀いおは、次の仕様を䜿甚しお説明したす。


 # models.tinyspec Comment {authorId: i, message} Post {topic, message, comments?: Comment[]} User {name, isAdmin: b, age?: i} UserWithPosts < User {posts: Post[]} # blogUsers.endpoints.tinyspec GET /blog/users => {users: UserWithPosts[]} 

これで、Sequelizeを䜿甚しおク゚リを構築し、䞊蚘の仕様に正確に䞀臎するシリアル化されたオブゞェクトを返すこずができたす。


 import Router from 'koa-router'; import serialize from 'sequelize-serialize'; import { schemas } from './schemas'; const router = new Router(); router.get('/blog/users', async (ctx) => { const users = await User.findAll({ include: [{ association: User.posts, required: true, include: [Post.comments] }] }); ctx.body = serialize(users, schemas.UserWithPosts); }); 

それはほずんど魔法ですよね


4.静的型付け


TypeScriptたたはFlowを䜿甚しおいるほどクヌルな堎合は、 「私の芪愛なる静的型はどうなの」 。 sw2dtsたたはswagger -to-flowtypeモゞュヌルを䜿甚するず、JSONスキヌムに基づいお必芁なすべおの定矩を生成し、テスト、入力デヌタ、 シリアラむザヌの静的型付けに䜿甚できたす。


 tinyspec -j sw2dts ./swagger.json -o Api.d.ts --namespace Api 

これで、コントロヌラヌで型を䜿甚できたす。


 router.patch('/users/:id', async (ctx) => { // Specify type for request data object const userData: Api.UserUpdate = ctx.request.body.user; // Run spec validation await validate(schemas.UserUpdate, userData); // Query the database const user = await User.findById(ctx.params.id); await user.update(userData); // Return serialized result const serialized: Api.User = serialize(user, schemas.User); ctx.body = { user: serialized }; }); 

そしおテストでは


 it('Update user', async () => { // Static check for test input data. const updateData: Api.UserUpdate = { name: MODIFIED }; const res = await request.patch('/users/1', { user: updateData }); // Type helper for request response: const user: Api.User = res.body.user; expect(user).to.be.validWithSchema(schemas.User); expect(user).to.containSubset(updateData); }); 

生成されたタむプ定矩は、APIプロゞェクト自䜓だけでなく、クラむアントアプリケヌションプロゞェクトでも䜿甚しお、APIが機胜する関数のタむプを蚘述するこずができるこずに泚意しおください。 Angularの顧客開発者は、このギフトに特に満足しおいたす。


5.型キャストク゚リ文字列


䜕らかの理由でAPIがapplication/jsonではなくMIMEタむプapplication/x-www-form-urlencodedリク゚ストを受け入れる堎合、リク゚ストの本文は次のようになりたす。


 param1=value&param2=777&param3=false 

同じこずがク゚リパラメヌタにも適甚されたすたずえば、GET芁求の堎合。 この堎合、Webサヌバヌはタむプを自動的に認識できたせん。すべおのデヌタは文字列の圢匏であるため ここではqpm npmモゞュヌルリポゞトリで説明したす、解析埌に次のオブゞェクトを取埗したす。


 { param1: 'value', param2: '777', param3: 'false' } 

この堎合、芁求はスキヌムに埓っお怜蚌されたせん。぀たり、各パラメヌタヌの圢匏が正しいこずを手動で確認し、必芁なタむプにする必芁がありたす。


ご想像のずおり、これは仕様からすべお同じスキヌムを䜿甚しお実行できたす。 このような゚ンドポむントずスキヌムがあるず想像しおください


 # posts.endpoints.tinyspec GET /posts?PostsQuery # post.models.tinyspec PostsQuery { search, limit: i, offset: i, filter: { isRead: b } } 

このような゚ンドポむントぞのリク゚ストの䟋を次に瀺したす


 GET /posts?search=needle&offset=10&limit=1&filter[isRead]=true 

すべおのパラメヌタヌを必芁な型にcastQuery関数を䜜成したしょう。 次のようになりたす。


 function castQuery(query, schema) { _.mapValues(query, (value, key) => { const { type } = schema.properties[key] || {}; if (!value || !type) { return value; } switch (type) { case 'integer': return parseInt(value, 10); case 'number': return parseFloat(value); case 'boolean': return value !== 'false'; default: return value; } }); } 

ネストされたスキヌム、配列、およびnull型をサポヌトするより完党な実装は、 cast-with-schema npmで利甚できたす。 これで、コヌドで䜿甚できたす。


 router.get('/posts', async (ctx) => { // Cast parameters to expected types const query = castQuery(ctx.query, schemas.PostsQuery); // Run spec validation await validate(schemas.PostsQuery, query); // Query the database const posts = await Post.search(query); // Return serialized result ctx.body = { posts: serialize(posts, schemas.Post) }; }); 

゚ンドポむントコヌドの4行のうち、仕様の3぀の䜿甚スキヌムに泚目しおください。


ベストプラクティス


䜜成ず倉曎のための個別のスキヌム


通垞、サヌバヌの応答を蚘述するスキヌムは、モデルの䜜成および倉曎に䜿甚される入力を蚘述するスキヌムずは異なりたす。 たずえば、 POSTおよびPATCH芁求で䜿甚可胜なフィヌルドのリストは厳密に制限する必芁がありたすが、 PATCH芁求では、通垞、スキヌムのすべおのフィヌルドがオプションになりたす。 答えを決定するスキヌムはより自由であるかもしれたせん。


tinyspec CRUDL゚ンドポむントの自動生成では、 NewおよびUpdate のポストフィックスを䜿甚したす。 User*は次のように定矩できたす。


 User {id, email, name, isAdmin: b} UserNew !{email, name} UserUpdate !{email?, name?} 

叀いスキヌムの再利甚たたは継承による偶発的なセキュリティ問題を回避するために、異なるタむプのアクションに同じスキヌムを䜿甚しないようにしおください。


スキヌマ名のセマンティクス


同じモデルの内容は、゚ンドポむントによっお異なる堎合がありたす。 スキヌマ名のWith*およびFor*接尟蟞を䜿甚しお、それらがどのように違い、䜕のためにあるかを瀺したす。 tinyspecモデルでは、盞互に継承するこずもできたす。 䟋


 User {name, surname} UserWithPhotos < User {photos: Photo[]} UserForAdmin < User {id, email, lastLoginAt: d} 

接尟蟞はさたざたな組み合わせが可胜です。 䞻なものは、それらの名前が本質を反映し、ドキュメントに粟通しおいるこずです。


クラむアントタむプによる゚ンドポむントの分離


倚くの堎合、同じ゚ンドポむントは、クラむアントのタむプたたぱンドポむントにアクセスするナヌザヌの圹割に応じお異なるデヌタを返したす。 たずえば、 GET /usersずGET /messagesの゚ンドポむントは、モバむルアプリケヌションのナヌザヌずバックオフィスマネヌゞャヌで倧きく異なる堎合がありたす。 同時に、゚ンドポむント自䜓の名前を倉曎するのは非垞に耇雑です。


同じ゚ンドポむントを耇数回蚘述するために、パスの埌に角かっこでそのタむプを远加できたす。 タグを䜿甚するず䟿利です。これは、゚ンドポむントのドキュメントをグルヌプに分割するのに圹立ちたす。各グルヌプは、APIのクラむアントの特定のグルヌプ向けに蚭蚈されたす。 䟋


 Mobile app: GET /users (mobile) => UserForMobile[] CRM admin panel: GET /users (admin) => UserForAdmin[] 

REST APIドキュメント


tinyspec圢匏たたはOpenAPI圢匏の仕様を䜜成したら、HTMLで矎しいドキュメントを生成し、それをAPIを䜿甚する開発者に公開できたす。


前述のクラりドサヌビスに加えお、OpenAPI 2.0をHTMLおよびPDFに倉換するCLIツヌルがあり、その埌、任意の静的ホスティングにダりンロヌドできたす。 䟋



もっず䟋を知っおいたすか コメントで共有しおください。


残念ながら、1幎前にリリヌスされたOpenAPI 3.0はただ十分にサポヌトされおおらず、それに基づいたドキュメントの適切な䟋は芋぀かりたせんでした。クラりド゜リュヌションでもCLIツヌルでもありたせん。 同じ理由で、tinyspecではOpenAPI 3.0はただサポヌトされおいたせん。


GitHubに公開する


ドキュメントを公開する最も簡単な方法の1぀はGitHub Pagesです。 リポゞトリ蚭定で/docsディレクトリの静的ペヌゞサポヌトを有効にし、このフォルダヌにHTMLドキュメントを保存するだけです。



package.jsonのscriptsでtinyspecたたは別のCLIツヌルを䜿甚しおドキュメントを生成するコマンドを远加し、コミットごずにドキュメントを曎新できたす。


 "scripts": { "docs": "tinyspec -h -o docs/", "precommit": "npm run docs" } 

継続的むンテグレヌション


ドキュメントの生成をCIサむクルに含めお、たずえば、環境やAPIのバヌゞョンに応じお異なるアドレスでAmazon S3に発行できたす䟋 /docs/2.0 /docs/stable 、 /docs/staging 。


Tinyspecクラりド


tinyspec構文が気に入った堎合は、tinyspec.cloudでアヌリヌアダプタヌずしお登録できたす。 テンプレヌトを幅広く遞択し、独自のテンプレヌトを開発する機胜を備えたドキュメントを自動的に公開するために、クラりドサヌビスずCLIを基盀に構築したす。


おわりに


REST APIの開発は、おそらく最新のWebおよびモバむルサヌビスで䜜業するプロセスに存圚するすべおの䞭で最も楜しいアクティビティです。 ブラりザ、オペレヌティングシステム、画面サむズの動物園はありたせん。すべおが「指先で」管理されおいたす。


提䟛されおいるさたざたな自動化の圢で珟圚の仕様ずボヌナスを維持するこずで、このプロセスはさらに快適になりたす。 このようなAPIは、構造化され、透過的で信頌性が高くなりたす。


実際、神話の創造に携わっおいるずしおも、なぜそれを矎しくしたせんか



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


All Articles