5 สิ่งที่คุณไม่เคยทำกับข้อกำหนด REST
เผยแพร่แล้ว: 2022-03-11นักพัฒนาส่วนหน้าและส่วนหลังส่วนใหญ่เคยจัดการกับข้อกำหนด REST และ RESTful API มาก่อน แต่ไม่ใช่ว่า RESTful API ทั้งหมดจะถูกสร้างขึ้นมาเท่ากัน อันที่จริงพวกมันไม่ค่อยพักผ่อนเลย...
RESTful API คือ อะไร?
มันเป็นตำนาน
หากคุณคิดว่าโครงการของคุณมี RESTful API คุณมักจะเข้าใจผิด แนวคิดเบื้องหลัง RESTful API คือการพัฒนาในลักษณะที่เป็นไปตามกฎสถาปัตยกรรมและข้อจำกัดทั้งหมดที่อธิบายไว้ในข้อกำหนด REST อย่างไรก็ตาม ตามความเป็นจริงแล้ว ในทางปฏิบัติแทบจะเป็นไปไม่ได้เลย
ในอีกด้านหนึ่ง REST มีคำจำกัดความที่ไม่ชัดเจนและคลุมเครือมากเกินไป ตัวอย่างเช่น ในทางปฏิบัติ มีการใช้คำศัพท์บางคำจากเมธอด HTTP และพจนานุกรมรหัสสถานะซึ่งขัดต่อวัตถุประสงค์ที่ตั้งใจไว้ หรือไม่ได้ใช้เลย
ในทางกลับกัน การพัฒนา REST สร้างข้อจำกัดมากเกินไป ตัวอย่างเช่น การใช้ทรัพยากรปรมาณูไม่เหมาะสมสำหรับ API ในโลกแห่งความเป็นจริงที่ใช้ในแอปพลิเคชันมือถือ การปฏิเสธการจัดเก็บข้อมูลระหว่างคำขอโดยสมบูรณ์เป็นการห้ามกลไก "เซสชันผู้ใช้" ที่เห็นได้ทุกที่
แต่เดี๋ยวก่อน มันไม่ได้เลวร้ายขนาดนั้น!
คุณต้องการข้อกำหนด REST API เพื่ออะไร
แม้จะมีข้อเสียเหล่านี้ แต่ด้วยวิธีการที่สมเหตุสมผล REST ยังคงเป็นแนวคิดที่ยอดเยี่ยมสำหรับการสร้าง API ที่ยอดเยี่ยมจริงๆ API เหล่านี้สามารถสอดคล้องกันและมีโครงสร้างที่ชัดเจน มีเอกสารประกอบที่ดี และครอบคลุมการทดสอบหน่วยในระดับสูง คุณสามารถบรรลุสิ่งนี้ได้ด้วย ข้อกำหนด API คุณภาพสูง
โดยปกติข้อกำหนด REST API จะเชื่อมโยงกับ เอกสารประกอบ ไม่เหมือนกับข้อกำหนด—คำอธิบายอย่างเป็นทางการของ API ของคุณ—เอกสารประกอบมีขึ้นเพื่อให้มนุษย์สามารถอ่านได้: ตัวอย่างเช่น นักพัฒนาของโมบายล์หรือเว็บแอปพลิเคชันที่ใช้ API ของคุณ
คำอธิบาย API ที่ถูกต้องไม่ใช่แค่การเขียนเอกสาร API เท่านั้น ในบทความนี้ ฉันต้องการแบ่งปันตัวอย่างเกี่ยวกับวิธีการ:
- ทำให้การทดสอบหน่วยของคุณง่ายขึ้นและเชื่อถือได้มากขึ้น
- ตั้งค่าการประมวลผลล่วงหน้าและการตรวจสอบการป้อนข้อมูลของผู้ใช้
- ทำให้การซีเรียลไลซ์เซชั่นเป็นอัตโนมัติและรับรองความสอดคล้องของการตอบสนอง และแม้กระทั่ง
- เพลิดเพลินไปกับประโยชน์ของการพิมพ์แบบคงที่
แต่ก่อนอื่น เรามาเริ่มด้วยการแนะนำโลกของข้อกำหนด API
OpenAPI
ปัจจุบัน OpenAPI เป็นรูปแบบที่ยอมรับกันอย่างกว้างขวางที่สุดสำหรับข้อกำหนด REST API ข้อมูลจำเพาะเขียนเป็นไฟล์เดียวในรูปแบบ JSON หรือ YAML ซึ่งประกอบด้วยสามส่วน:
- ส่วนหัวที่มีชื่อ API คำอธิบายและเวอร์ชัน ตลอดจนข้อมูลเพิ่มเติม
- คำอธิบายของทรัพยากรทั้งหมด รวมถึงตัวระบุ เมธอด HTTP พารามิเตอร์อินพุตทั้งหมด รหัสตอบกลับ และประเภทข้อมูลเนื้อหา พร้อมลิงก์ไปยังคำจำกัดความ
- คำจำกัดความทั้งหมดที่สามารถใช้ได้สำหรับอินพุตหรือเอาต์พุต ในรูปแบบ JSON Schema (ซึ่งใช่ สามารถแสดงใน YAML ได้ด้วย)
โครงสร้างของ OpenAPI มีข้อเสียที่สำคัญสองประการ: มันซับซ้อนเกินไปและบางครั้งก็ซ้ำซ้อน โปรเจ็กต์ขนาดเล็กสามารถมีข้อกำหนด JSON ได้หลายพันบรรทัด การรักษาไฟล์นี้ด้วยตนเองจะเป็นไปไม่ได้ นี่เป็นภัยคุกคามที่สำคัญต่อแนวคิดในการทำให้ข้อมูลจำเพาะเป็นปัจจุบันในขณะที่ API กำลังอยู่ระหว่างการพัฒนา
มีเอดิเตอร์หลายตัวที่อนุญาตให้คุณอธิบาย API และสร้างเอาต์พุต OpenAPI บริการเพิ่มเติมและโซลูชันระบบคลาวด์ที่อิงจากบริการเหล่านี้ ได้แก่ Swagger, Apiary, Stoplight, Restlet และอื่นๆ อีกมากมาย
อย่างไรก็ตาม บริการเหล่านี้ไม่สะดวกสำหรับฉันเนื่องจากความซับซ้อนของการแก้ไขข้อกำหนดอย่างรวดเร็วและการปรับให้เข้ากับการเปลี่ยนแปลงโค้ด นอกจากนี้ รายการคุณสมบัติยังขึ้นอยู่กับบริการเฉพาะ ตัวอย่างเช่น การสร้างการทดสอบหน่วยที่เต็มเปี่ยมโดยอิงจากเครื่องมือของบริการคลาวด์นั้นแทบจะเป็นไปไม่ได้เลย การสร้างโค้ดและการเยาะเย้ยจุดสิ้นสุด กลับกลายเป็นว่าไม่มีประโยชน์ในทางปฏิบัติเป็นส่วนใหญ่ สาเหตุส่วนใหญ่เป็นเพราะพฤติกรรมปลายทางมักจะขึ้นอยู่กับสิ่งต่าง ๆ เช่น การอนุญาตของผู้ใช้และพารามิเตอร์อินพุต ซึ่งอาจชัดเจนสำหรับสถาปนิก API แต่สร้างโดยอัตโนมัติจากข้อมูลจำเพาะของ OpenAPI ไม่ได้ง่าย
Tinyspec
ในบทความนี้ ฉันจะใช้ตัวอย่างตามรูปแบบคำจำกัดความ REST API ของฉันเอง tinyspec คำจำกัดความประกอบด้วยไฟล์ขนาดเล็กที่มีไวยากรณ์ที่เข้าใจง่าย อธิบายปลายทางและแบบจำลองข้อมูลที่ใช้ในโครงการ ไฟล์จะถูกเก็บไว้ข้างๆ โค้ด ช่วยให้อ้างอิงได้อย่างรวดเร็วและสามารถแก้ไขได้ระหว่างการเขียนโค้ด Tinyspec ได้รับการคอมไพล์ให้เป็นรูปแบบ OpenAPI เต็มรูปแบบโดยอัตโนมัติ ซึ่งสามารถใช้ในโครงการของคุณได้ทันที
ฉันจะใช้ตัวอย่าง Node.js (Koa, Express) และ Ruby on Rails ด้วย แต่แนวทางปฏิบัติที่ฉันจะแสดงนั้นใช้ได้กับเทคโนโลยีส่วนใหญ่ รวมถึง Python, PHP และ Java
ที่ API Specification Rocks
ตอนนี้เรามีพื้นฐานแล้ว เราสามารถสำรวจวิธีใช้ประโยชน์สูงสุดจาก API ที่ระบุอย่างเหมาะสมได้
1. การทดสอบหน่วยปลายทาง
การพัฒนาที่ขับเคลื่อนด้วยพฤติกรรม (BDD) เหมาะอย่างยิ่งสำหรับการพัฒนา REST API เป็นการดีที่สุดที่จะเขียนการทดสอบหน่วยไม่ใช่สำหรับคลาส โมเดล หรือคอนโทรลเลอร์ที่แยกจากกัน แต่สำหรับปลายทางเฉพาะ ในการทดสอบแต่ละครั้ง คุณจำลองคำขอ HTTP จริงและตรวจสอบการตอบสนองของเซิร์ฟเวอร์ สำหรับ Node.js มีแพ็คเกจ supertest และ chai-http สำหรับการจำลองคำขอ และสำหรับ Ruby on Rails จะมีบริการทางอากาศ
สมมติว่าเรามี User
schema และ GET /users
endpoint ที่ส่งคืนผู้ใช้ทั้งหมด นี่คือไวยากรณ์ของ tinyspec ที่อธิบายสิ่งนี้:
# user.models.tinyspec User {name, isAdmin: b, age?: i} # 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]); }); });
ทับทิมบนราง
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 ที่เป็นไปตามรูปแบบ JSON Schema
อ็อบเจกต์ตามตัวอักษรใดๆ ใน JS (หรือ Hash
ใน Ruby, dict
ใน Python, associative array ใน PHP และแม้แต่ Map
ใน Java) สามารถตรวจสอบได้เพื่อให้สอดคล้องกับ JSON Schema มีปลั๊กอินที่เหมาะสมสำหรับการทดสอบเฟรมเวิร์ก เช่น jest-ajv (npm), chai-ajv-json-schema (npm) และ json_matchers สำหรับ RSpec (rubygem)
ก่อนใช้สคีมา ให้นำเข้ามาไว้ในโปรเจ็กต์ก่อน ขั้นแรก ให้สร้างไฟล์ openapi.json
ตามข้อกำหนดของ tinyspec (คุณสามารถทำได้โดยอัตโนมัติก่อนทำการทดสอบแต่ละครั้ง):
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); }); });
ทับทิมบนราง
โมดูล json_matchers
รู้วิธีจัดการกับการอ้างอิง $ref
แต่ต้องการไฟล์ schema แยกกันในตำแหน่งที่ระบุ ดังนั้นคุณจะต้องแยกไฟล์ 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 ทั้งหมดถูกจำกัดไว้ที่สามขั้นตอน:
- การออกแบบข้อกำหนดในไฟล์ tinyspec
- การเขียนชุดการทดสอบที่สมบูรณ์สำหรับปลายทางที่เพิ่ม/แก้ไข
- การใช้รหัสที่ตรงตามการทดสอบ
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—แต่โค้ด 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
:
# error.models.tinyspec Error {error: b, message} InvalidField {name, message} FieldsValidationError < Error {fields: InvalidField[]}
และตอนนี้ เรามาแสดงรายการเป็นหนึ่งในการตอบสนองปลายทางที่เป็นไปได้:
# users.endpoints.tinyspec PATCH /users/:id {user: UserUpdate} => 200 {success: b} => 422 FieldsValidationError
วิธีนี้ช่วยให้คุณเขียนการทดสอบหน่วยที่ทดสอบความถูกต้องของสถานการณ์ข้อผิดพลาดเมื่อข้อมูลที่ไม่ถูกต้องมาจากไคลเอ็นต์
3. Model Serialization
กรอบงานเซิร์ฟเวอร์สมัยใหม่เกือบทั้งหมดใช้การแมปเชิงวัตถุ (ORM) ไม่ทางใดก็ทางหนึ่ง ซึ่งหมายความว่าทรัพยากรส่วนใหญ่ที่ API ใช้นั้นแสดงโดยโมเดลและอินสแตนซ์และคอลเล็กชัน

กระบวนการสร้างการแสดงแทน JSON สำหรับเอนทิตีเหล่านี้ที่จะส่งในการตอบกลับเรียกว่าการทำให้เป็น อันดับ
มีปลั๊กอินจำนวนมากสำหรับการทำซีเรียลไลซ์เซชัน: ตัวอย่างเช่น sequelize-to-json (npm), actions_as_api (rubygem) และ jsonapi-rails (rubygem) โดยพื้นฐานแล้ว ปลั๊กอินเหล่านี้ช่วยให้คุณสามารถระบุรายการฟิลด์สำหรับโมเดลเฉพาะที่ต้องรวมอยู่ในออบเจ็กต์ JSON รวมถึงกฎเพิ่มเติม ตัวอย่างเช่น คุณสามารถเปลี่ยนชื่อฟิลด์และคำนวณค่าไดนามิก
จะยากขึ้นเมื่อคุณต้องการการแสดง JSON ที่แตกต่างกันหลายแบบสำหรับโมเดลเดียว หรือเมื่ออ็อบเจ็กต์มีเอนทิตีที่ซ้อนกัน—การเชื่อมโยง จากนั้นคุณก็เริ่มต้องการคุณสมบัติต่างๆ เช่น การสืบทอด การใช้ซ้ำ และการเชื่อมโยงซีเรียลไลเซอร์
โมดูลที่ต่างกันมีวิธีแก้ปัญหาที่แตกต่างกัน แต่ลองมาพิจารณากันดู: ข้อมูลจำเพาะสามารถช่วยได้อีกครั้งหรือไม่ โดยพื้นฐานแล้ว ข้อมูลทั้งหมดเกี่ยวกับข้อกำหนดสำหรับการแสดง JSON ชุดค่าผสมของฟิลด์ที่เป็นไปได้ทั้งหมด รวมถึงเอนทิตีที่ฝังตัวมีอยู่แล้วในนั้น และนี่หมายความว่าเราสามารถเขียนซีเรียลไลเซอร์อัตโนมัติตัวเดียวได้
ให้ฉันนำเสนอโมดูล sequelize-serialize (npm) ขนาดเล็ก ซึ่งรองรับการทำเช่นนี้สำหรับโมเดล Sequelize ยอมรับอินสแตนซ์ของโมเดลหรืออาร์เรย์ และสคีมาที่จำเป็น จากนั้นวนซ้ำเพื่อสร้างออบเจ็กต์ที่ทำให้เป็นอนุกรม นอกจากนี้ยังบัญชีสำหรับฟิลด์ที่จำเป็นทั้งหมดและใช้ schema ที่ซ้อนกันสำหรับเอนทิตีที่เกี่ยวข้อง
สมมติว่าเราจำเป็นต้องส่งคืนผู้ใช้ทั้งหมดที่มีโพสต์ในบล็อก รวมทั้งความคิดเห็นในโพสต์เหล่านี้จาก 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 ได้อีกด้วย (นักพัฒนาเชิงมุมจะมีความสุขกับสิ่งนี้เป็นพิเศษ)
5. การคัดเลือกประเภทสตริงการสืบค้น
หาก API ของคุณใช้คำขอด้วยประเภท MIME ของ application/x-www-form-urlencoded
MIME แทน application/json
เนื้อหาคำขอจะมีลักษณะดังนี้:
param1=value¶m2=777¶m3=false
เช่นเดียวกับพารามิเตอร์การสืบค้น (เช่น ในคำขอ GET
) ในกรณีนี้ เว็บเซิร์ฟเวอร์จะไม่รู้จักประเภทโดยอัตโนมัติ: ข้อมูลทั้งหมดจะอยู่ในรูปแบบสตริง ดังนั้นหลังจากแยกวิเคราะห์แล้ว คุณจะได้วัตถุนี้:
{ 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) }; });
โปรดทราบว่าโค้ดสามในสี่บรรทัดใช้สคีมาข้อกำหนด
ปฏิบัติที่ดีที่สุด
มีแนวทางปฏิบัติที่ดีที่สุดหลายประการที่เราสามารถปฏิบัติตามได้ที่นี่
ใช้การสร้างและแก้ไข Schema แยกกัน
โดยปกติ schema ที่อธิบายการตอบสนองของเซิร์ฟเวอร์จะแตกต่างจากที่อธิบายอินพุต และใช้เพื่อสร้างและแก้ไขโมเดล ตัวอย่างเช่น รายการของฟิลด์ที่มีอยู่ใน POST
และ PATCH
จะต้องถูกจำกัดอย่างเข้มงวด และ PATCH
มักจะมีฟิลด์ทั้งหมดที่ทำเครื่องหมายว่าเป็นทางเลือก สคีมาที่อธิบายการตอบสนองสามารถเป็นแบบอิสระมากขึ้น
เมื่อคุณสร้าง CRUDL endpoints โดยอัตโนมัติ tinyspec จะใช้ New
และ Update
postfixes User*
schema สามารถกำหนดได้ดังนี้:
User {id, email, name, isAdmin: b} UserNew !{email, name} UserUpdate !{email?, name?}
พยายามอย่าใช้สคีมาเดียวกันสำหรับการดำเนินการประเภทต่างๆ เพื่อหลีกเลี่ยงปัญหาด้านความปลอดภัยโดยไม่ได้ตั้งใจอันเนื่องมาจากการใช้ซ้ำหรือการสืบทอดสกีมารุ่นเก่า
ปฏิบัติตามข้อตกลงการตั้งชื่อสคีมา
เนื้อหาของรุ่นเดียวกันอาจแตกต่างกันไปตามจุดสิ้นสุดที่ต่างกัน ใช้ With*
และ For*
postfixes ในชื่อสคีมาเพื่อแสดงความแตกต่างและวัตถุประสงค์ ใน tinyspec โมเดลยังสามารถสืบทอดจากกันได้ ตัวอย่างเช่น:
User {name, surname} UserWithPhotos < User {photos: Photo[]} UserForAdmin < User {id, email, lastLoginAt: d}
Postfixes สามารถเปลี่ยนแปลงและรวมกันได้ ชื่อของพวกเขายังคงสะท้อนถึงแก่นแท้และทำให้เอกสารอ่านง่ายขึ้น
การแยกปลายทางตามประเภทไคลเอ็นต์
บ่อยครั้งที่ปลายทางเดียวกันส่งคืนข้อมูลที่แตกต่างกันตามประเภทไคลเอนต์หรือบทบาทของผู้ใช้ที่ส่งคำขอ ตัวอย่างเช่น ปลายทาง GET /users
และ GET /messages
อาจแตกต่างกันอย่างมากสำหรับผู้ใช้แอปพลิเคชันมือถือและผู้จัดการฝ่ายสนับสนุน การเปลี่ยนชื่อปลายทางอาจเป็นค่าใช้จ่าย
หากต้องการอธิบายปลายทางเดียวกันหลายๆ ครั้ง คุณสามารถเพิ่มประเภทปลายทางในวงเล็บหลังเส้นทางได้ สิ่งนี้ยังทำให้แท็กใช้งานง่ายอีกด้วย: คุณแบ่งเอกสารปลายทางออกเป็นกลุ่มๆ ซึ่งแต่ละกลุ่มมีไว้สำหรับกลุ่มไคลเอ็นต์ API เฉพาะ ตัวอย่างเช่น:
Mobile app: GET /users (mobile) => UserForMobile[] CRM admin panel: GET /users (admin) => UserForAdmin[]
เครื่องมือเอกสาร REST API
หลังจากที่คุณได้รับข้อมูลจำเพาะในรูปแบบ tinyspec หรือ OpenAPI คุณสามารถสร้างเอกสารที่ดูดีในรูปแบบ HTML และเผยแพร่ได้ วิธีนี้จะทำให้นักพัฒนาที่ใช้ API ของคุณมีความสุข และแน่นอนว่าต้องกรอกแม่แบบเอกสาร REST API ด้วยมือ
นอกเหนือจากบริการคลาวด์ที่กล่าวถึงก่อนหน้านี้แล้ว ยังมีเครื่องมือ CLI ที่แปลง OpenAPI 2.0 เป็น HTML และ PDF ซึ่งสามารถปรับใช้กับโฮสติ้งแบบคงที่ใดๆ ก็ได้ นี่คือตัวอย่างบางส่วน:
- bootprint-openapi (npm ใช้โดยค่าเริ่มต้นใน tinyspec)
- swagger2markup-cli (โถ มีตัวอย่างการใช้งาน จะใช้ใน tinyspec Cloud)
- redoc-cli (npm)
- วิดเดอร์ชิน (npm)
คุณมีตัวอย่างเพิ่มเติมหรือไม่? แบ่งปันในความคิดเห็น
น่าเศร้า แม้ว่า OpenAPI 3.0 จะออกวางจำหน่ายเมื่อปีที่แล้ว แต่ก็ยังได้รับการสนับสนุนที่ไม่ดี และฉันไม่พบตัวอย่างเอกสารที่เหมาะสมโดยอิงจากมันทั้งในโซลูชันระบบคลาวด์และในเครื่องมือ CLI ด้วยเหตุผลเดียวกัน tinyspec ยังไม่รองรับ OpenAPI 3.0
เผยแพร่บน GitHub
วิธีที่ง่ายที่สุดวิธีหนึ่งในการเผยแพร่เอกสารคือ GitHub Pages เพียงเปิดใช้งานการสนับสนุนสำหรับหน้าสแตติกสำหรับโฟลเดอร์ /docs
ของคุณในการตั้งค่าที่เก็บ และจัดเก็บเอกสาร HTML ในโฟลเดอร์นี้
คุณสามารถเพิ่มคำสั่งเพื่อสร้างเอกสารผ่าน tinyspec หรือเครื่องมือ CLI อื่นใน scripts/package.json
ของคุณเพื่ออัปเดตเอกสารโดยอัตโนมัติหลังจากการคอมมิตแต่ละครั้ง:
"scripts": { "docs": "tinyspec -h -o docs/", "precommit": "npm run docs" }
บูรณาการอย่างต่อเนื่อง
คุณสามารถเพิ่มการสร้างเอกสารให้กับวงจร CI ของคุณและเผยแพร่ได้ ตัวอย่างเช่น ไปยัง Amazon S3 ภายใต้ที่อยู่ที่แตกต่างกันขึ้นอยู่กับสภาพแวดล้อมหรือเวอร์ชัน API (เช่น /docs/2.0
, /docs/stable
และ /docs/staging
)
Tinyspec Cloud
หากคุณชอบไวยากรณ์ของ tinyspec คุณสามารถเป็นผู้ปรับใช้ในช่วงต้นของ tinyspec.cloud ได้ เราวางแผนที่จะสร้างบริการคลาวด์โดยอิงจากบริการดังกล่าวและ CLI สำหรับการปรับใช้เอกสารโดยอัตโนมัติด้วยเทมเพลตที่มีให้เลือกมากมายและความสามารถในการพัฒนาเทมเพลตส่วนบุคคล
ข้อกำหนด REST: A Marvelous Myth
การพัฒนา REST API ถือเป็นหนึ่งในกระบวนการที่น่าพึงพอใจที่สุดในการพัฒนาบริการบนเว็บและมือถือที่ทันสมัย ไม่มีเบราว์เซอร์ ระบบปฏิบัติการ และสวนสัตว์ขนาดหน้าจอ และทุกอย่างอยู่ภายใต้การควบคุมของคุณเพียงปลายนิ้วสัมผัส
กระบวนการนี้ทำได้ง่ายยิ่งขึ้นด้วยการสนับสนุนระบบอัตโนมัติและข้อกำหนดที่เป็นปัจจุบัน API ที่ใช้วิธีที่ฉันอธิบายจะมีโครงสร้างที่ดี โปร่งใส และเชื่อถือได้
สิ่งที่สำคัญที่สุดคือถ้าเรากำลังสร้างตำนาน ทำไมไม่ทำให้มันเป็นตำนานที่น่าอัศจรรย์ล่ะ?