Artikel ini akan fokus pada penulisan dan mendukung spesifikasi yang berguna dan relevan untuk proyek REST API, yang akan menghemat banyak kode tambahan, serta secara serius meningkatkan integritas, keandalan, dan transparansi proyek secara keseluruhan.
Apa itu API tenang?
Ini hanya mitos.
Serius, jika Anda berpikir proyek Anda memiliki API RESTful, Anda hampir pasti salah. Gagasan RESTful adalah membangun API yang dalam segala hal mematuhi aturan dan batasan arsitektur yang dijelaskan oleh gaya REST, tetapi dalam kondisi nyata hal ini hampir mustahil .
Di satu sisi, REST mengandung terlalu banyak definisi yang kabur dan ambigu. Sebagai contoh, beberapa istilah dari kamus tentang metode HTTP dan kode status tidak digunakan untuk tujuan yang dimaksudkan dalam praktiknya, sementara banyak dari mereka tidak digunakan sama sekali.
Di sisi lain, REST menciptakan terlalu banyak batasan. Sebagai contoh, penggunaan sumber daya atom di dunia nyata tidak rasional untuk API yang digunakan oleh aplikasi seluler. Penolakan lengkap untuk menyimpan status di antara permintaan pada dasarnya adalah larangan mekanisme sesi pengguna yang digunakan di banyak API.
Tapi tunggu, tidak semuanya begitu buruk!
Mengapa kita memerlukan spesifikasi REST API?
Terlepas dari kekurangan ini, dengan pendekatan yang masuk akal, REST tetap menjadi dasar yang sangat baik untuk merancang API yang sangat keren. API semacam itu harus memiliki keseragaman internal, struktur yang jelas, dokumentasi yang nyaman dan cakupan uji unit yang baik. Semua ini dapat dicapai dengan mengembangkan spesifikasi kualitas untuk API Anda.
Paling sering, spesifikasi REST API dikaitkan dengan dokumentasinya . Berbeda dengan yang pertama (yang merupakan deskripsi formal API Anda), dokumentasi ini dimaksudkan untuk dibaca oleh orang-orang: misalnya, pengembang aplikasi seluler atau web menggunakan API Anda.
Namun, selain benar-benar membuat dokumentasi, deskripsi API yang tepat masih dapat membawa banyak manfaat. Dalam artikel ini saya ingin berbagi contoh bagaimana, dengan menggunakan spesifikasi yang kompeten, Anda dapat:
- membuat pengujian unit lebih sederhana dan lebih dapat diandalkan;
- mengkonfigurasi preprocessing dan validasi data input;
- mengotomatiskan serialisasi dan memastikan integritas respons;
- dan bahkan memanfaatkan pengetikan statis.
Openapi
Format yang diterima secara umum untuk menggambarkan API REST hari ini adalah OpenAPI , yang juga dikenal sebagai Swagger . Spesifikasi ini adalah satu file dalam format JSON atau YAML, yang terdiri dari tiga bagian:
- header yang berisi nama, deskripsi, dan versi API, serta informasi tambahan;
- deskripsi semua sumber daya, termasuk pengidentifikasi, metode HTTP, semua parameter input, serta kode dan format badan respons, dengan tautan ke definisi;
- semua definisi objek dalam format Skema JSON yang dapat digunakan baik dalam parameter input maupun respons.
OpenAPI memiliki kelemahan serius - kompleksitas struktur dan, seringkali, redundansi . Untuk proyek kecil, isi file spesifikasi JSON dapat dengan cepat tumbuh hingga beberapa ribu baris. File ini tidak mungkin disimpan secara manual dalam formulir ini. Ini adalah ancaman serius terhadap gagasan mempertahankan spesifikasi terbaru saat API berevolusi.
Ada banyak editor visual yang memungkinkan Anda untuk menggambarkan API dan membentuk spesifikasi OpenAPI yang dihasilkan. Pada gilirannya, layanan tambahan dan solusi cloud didasarkan pada mereka, misalnya, Swagger , Apiary , Stoplight , Restlet, dan lainnya.
Namun, bagi saya, layanan seperti itu sangat tidak nyaman karena sulitnya mengedit spesifikasi dan menggabungkannya dengan proses penulisan kode. Minus lainnya adalah ketergantungan pada set fungsi masing-masing layanan tertentu. Sebagai contoh, hampir tidak mungkin untuk mengimplementasikan pengujian unit lengkap hanya melalui layanan cloud. Pembuatan kode dan bahkan pembuatan "colokan" untuk titik akhir, meskipun tampaknya sangat mungkin, praktis tidak berguna dalam praktiknya.
Tinyspec
Pada artikel ini, saya akan menggunakan contoh berdasarkan format deskripsi REST API asli - tinyspec . Formatnya adalah file kecil yang menggambarkan titik akhir dan model data yang digunakan dalam proyek dengan sintaks yang intuitif. File disimpan di sebelah kode, yang memungkinkan Anda untuk memeriksanya dan mengeditnya tepat saat proses penulisan. Pada saat yang sama, tinyspec secara otomatis dikompilasi menjadi OpenAPI yang lengkap, yang dapat segera digunakan dalam proyek. Saatnya untuk memberi tahu Anda bagaimana tepatnya.
Dalam artikel ini, saya akan memberikan contoh dari Node.js (koa, express) dan Ruby on Rails, meskipun praktik ini berlaku untuk sebagian besar teknologi, termasuk Python, PHP, dan Java.
Ketika spec sangat berguna
1. Tes unit titik akhir
Pengembangan yang didorong oleh perilaku (BDD) sangat ideal untuk mengembangkan REST API. Cara yang paling mudah untuk menulis tes unit bukan untuk kelas individu, model dan pengontrol, tetapi untuk titik akhir tertentu. Di setiap pengujian, Anda meniru permintaan HTTP nyata dan memeriksa respons server. Di Node.js, untuk meniru permintaan tes, ada supertest dan chai-http , di Ruby on Rails - airborne .
Misalkan kita memiliki skema User
dan titik akhir GET /users
yang mengembalikan semua pengguna. Berikut adalah sintaksis tinyspec yang menjelaskan ini:
- File User.models.tinyspec :
User {name, isAdmin: b, age?: i}
- File users.endpoints.tinyspec :
GET /users => {users: User[]}
Begini tampilan pengujian kami:
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
Saat kami memiliki spesifikasi yang menjelaskan format respons server, kami dapat menyederhanakan pengujian dan cukup memeriksa respons terhadap spesifikasi ini . Untuk melakukan ini, kami akan mengambil keuntungan dari fakta bahwa model tinyspec kami diubah menjadi definisi OpenAPI, yang pada gilirannya sesuai dengan format Skema JSON.
Objek literal apa pun di JS (atau Hash
di Ruby, dict
dengan Python, array asosiatif dalam PHP, dan bahkan Map
di Jawa) dapat diuji kepatuhannya dengan skema JSON. Dan bahkan ada plugin yang sesuai untuk kerangka pengujian, misalnya jest-ajv (npm), chai-ajv-json-schema (npm) dan json_matchers (rubygem) untuk RSpec.
Sebelum menggunakan skema, Anda harus menghubungkannya ke proyek. Pertama, kita akan membuat file spesifikasi openapi.json berdasarkan tinyspec (tindakan ini dapat secara otomatis dilakukan sebelum setiap percobaan dijalankan):
tinyspec -j -o openapi.json
Node.js
Sekarang kita dapat menggunakan JSON yang diterima dalam proyek dan mengambil kunci definitions
darinya, yang berisi semua skema JSON. Skema dapat berisi referensi silang ( $ref
), oleh karena itu, jika kita memiliki skema bersarang (misalnya, Blog {posts: Post[]}
), maka kita perlu "memperluas" mereka untuk menggunakannya dalam validasi. Untuk melakukan ini, kita akan menggunakan 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);
Ruby on Rails
json_matchers
dapat menangani $ref
links, tetapi membutuhkan file terpisah dengan skema dalam sistem file dengan cara tertentu, jadi pertama-tama Anda harus "membagi" swagger.json
menjadi banyak file kecil (lebih lanjut tentang ini di sini ):
Setelah itu, kita dapat menulis tes kita seperti ini:
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
Catatan: menulis tes dengan cara ini sangat nyaman. Terutama jika IDE Anda mendukung tes yang sedang berjalan dan debugging (seperti WebStorm, RubyMine, dan Visual Studio). Dengan demikian, Anda tidak dapat menggunakan perangkat lunak lain sama sekali, dan seluruh siklus pengembangan API dikurangi menjadi 3 langkah berturut-turut:
- desain spesifikasi (mis. dalam tinyspec);
- menulis serangkaian tes lengkap untuk titik akhir yang ditambahkan / diubah;
- mengembangkan kode yang memenuhi semua tes.
2. Validasi input
OpenAPI menggambarkan format tidak hanya tanggapan, tetapi juga memasukkan data. Ini memungkinkan kami untuk memvalidasi data yang diterima dari pengguna saat permintaan.
Misalkan kita memiliki spesifikasi berikut yang menjelaskan pembaruan data pengguna, serta semua bidang yang dapat diubah:
Sebelumnya kami melihat plugin untuk validasi di dalam tes, namun untuk kasus yang lebih umum ada modul validasi ajv (npm) dan json-schema (rubygem), mari kita gunakan dan tulis controller dengan validasi.
Node.js (Koa)
Ini adalah contoh untuk Koa , penerus Express, tetapi untuk Express, kodenya akan terlihat serupa.
import Router from 'koa-router'; import Ajv from 'ajv'; import { schemas } from './schemas'; const router = new Router();
Dalam contoh ini, jika data input tidak memenuhi spesifikasi, server akan mengembalikan respons 500 Internal Server Error
kepada klien. Untuk mencegah hal ini terjadi, kami dapat mencegat kesalahan validator dan membentuk respons kami sendiri, yang akan berisi informasi lebih rinci tentang bidang tertentu yang belum lulus tes, dan juga mematuhi spesifikasi .
Tambahkan deskripsi model FieldsValidationError
di file FieldsValidationError
:
Error {error: b, message} InvalidField {name, message} FieldsValidationError < Error {fields: InvalidField[]}
Dan sekarang kami menunjukkannya sebagai salah satu jawaban yang mungkin dari titik akhir kami:
PATCH /users/:id {user: UserUpdate} => 200 {success: b} => 422 FieldsValidationError
Pendekatan ini akan memungkinkan Anda untuk menulis unit test yang memverifikasi kebenaran pembentukan kesalahan dengan data yang salah diterima dari klien.
3. Serialisasi model
Hampir semua kerangka kerja server modern menggunakan ORM dengan satu atau lain cara. Ini berarti bahwa sebagian besar sumber daya yang digunakan dalam API di dalam sistem disajikan dalam bentuk model, instans dan koleksi mereka.
Proses menghasilkan representasi JSON dari entitas ini untuk transmisi dalam respons API disebut serialisasi . Ada beberapa plugin untuk kerangka kerja berbeda yang melakukan fungsi serialisasi, misalnya: sequelize-to-json (npm), act_as_api (rubygem), jsonapi-rails (rubygem). Bahkan, plugin ini memungkinkan model tertentu untuk menentukan daftar bidang yang harus dimasukkan dalam objek JSON, serta aturan tambahan, misalnya, untuk mengganti nama atau secara dinamis menghitung nilai.
Kesulitan dimulai ketika kita perlu memiliki beberapa representasi JSON berbeda dari model yang sama atau ketika suatu objek berisi entitas bersarang - asosiasi. Ada kebutuhan untuk pewarisan, penggunaan kembali, dan penautan serialis .
Modul yang berbeda menyelesaikan masalah ini dengan cara yang berbeda, tetapi mari kita berpikir, dapatkah spesifikasi membantu kita lagi? Memang, pada kenyataannya, semua informasi tentang persyaratan untuk representasi JSON, semua kemungkinan kombinasi bidang, termasuk entitas bersarang, sudah ada di dalamnya. Jadi kita bisa menulis serializer otomatis.
Saya membawa perhatian Anda pada modul sekuel-serialisasi kecil (npm), yang memungkinkan Anda melakukan ini untuk model Sequelize. Ini mengambil contoh dari model atau array, serta sirkuit yang diperlukan, dan iteratif membangun objek serial, dengan mempertimbangkan semua bidang yang diperlukan dan menggunakan sirkuit bersarang untuk entitas terkait.
Jadi, anggaplah kita harus kembali dari API semua pengguna yang memiliki posting blog, termasuk komentar pada posting tersebut. Kami menggambarkan ini menggunakan spesifikasi berikut:
Sekarang kita dapat membangun kueri menggunakan Sequelize dan mengembalikan objek berseri yang sama persis dengan spesifikasi yang baru saja dijelaskan di atas:
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); });
Ini hampir ajaib, bukan?
4. Pengetikan statis
Jika Anda sangat keren sehingga Anda menggunakan TypeScript atau Flow, Anda mungkin sudah bertanya-tanya, "Bagaimana dengan tipe statis tersayang?!" . Menggunakan modul sw2dts atau swagger -to-flowtype, Anda dapat menghasilkan semua definisi yang diperlukan berdasarkan skema JSON dan menggunakannya untuk mengetik tes statis, input data dan serializer.
tinyspec -j sw2dts ./swagger.json -o Api.d.ts --namespace Api
Sekarang kita bisa menggunakan tipe di controller:
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 }; });
Dan dalam tes:
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); });
Harap perhatikan bahwa definisi tipe yang dihasilkan dapat digunakan tidak hanya dalam proyek API itu sendiri, tetapi juga dalam proyek aplikasi klien untuk menggambarkan jenis fungsi di mana API bekerja. Pengembang pelanggan angular akan sangat senang dengan hadiah ini.
5. Ketikkan konversi string kueri
Jika karena alasan tertentu API Anda menerima permintaan dengan jenis application/x-www-form-urlencoded
MIME application/x-www-form-urlencoded
dan bukan application/json
, badan permintaan akan terlihat seperti ini:
param1=value¶m2=777¶m3=false
Hal yang sama berlaku untuk parameter kueri (misalnya, dalam permintaan GET). Dalam hal ini, server web tidak akan dapat mengenali jenis secara otomatis - semua data akan berada dalam bentuk string (di sini ada diskusi dalam repositori modul qpm npm), jadi setelah parsing Anda akan mendapatkan objek berikut:
{ param1: 'value', param2: '777', param3: 'false' }
Dalam hal ini, permintaan tidak akan divalidasi sesuai dengan skema, yang berarti bahwa perlu secara manual memverifikasi bahwa setiap parameter memiliki format yang benar dan membawanya ke jenis yang diperlukan.
Seperti yang Anda duga, ini dapat dilakukan dengan menggunakan semua skema yang sama dari spesifikasi kami. Bayangkan kita memiliki titik akhir dan skema seperti itu:
Berikut adalah contoh permintaan ke titik akhir tersebut
GET /posts?search=needle&offset=10&limit=1&filter[isRead]=true
Mari kita menulis fungsi castQuery
, yang akan castQuery
semua parameter ke tipe yang diperlukan untuk kita. Akan terlihat seperti ini:
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; } }); }
Implementasinya yang lebih lengkap dengan dukungan untuk skema bersarang, array, dan tipe null
tersedia dalam skema cast-with-schema (npm). Sekarang kita dapat menggunakannya dalam kode kita:
router.get('/posts', async (ctx) => {
Perhatikan bagaimana dari empat baris kode titik akhir, tiga skema penggunaan dari spesifikasi.
Praktik terbaik
Skema terpisah untuk membuat dan memodifikasi
Biasanya, skema yang menggambarkan respons server berbeda dari skema yang menggambarkan input yang digunakan untuk membuat dan memodifikasi model. Misalnya, daftar bidang yang tersedia untuk permintaan POST
dan PATCH
harus dibatasi secara ketat, sedangkan dalam permintaan PATCH
, biasanya semua bidang skema dibuat opsional. Skema yang menentukan jawabannya mungkin lebih gratis.
Generasi otomatis titik akhir tinyspec CRUDL menggunakan postfix New
dan Update
. User*
dapat didefinisikan sebagai berikut:
User {id, email, name, isAdmin: b} UserNew !{email, name} UserUpdate !{email?, name?}
Cobalah untuk tidak menggunakan skema yang sama untuk berbagai jenis tindakan untuk menghindari masalah keamanan yang tidak disengaja karena penggunaan kembali atau warisan skema lama.
Semantik dalam nama skema
Isi dari model yang sama dapat bervariasi di titik akhir yang berbeda. Gunakan postfix With*
dan For*
dalam nama skema untuk menunjukkan bagaimana mereka berbeda dan untuk apa mereka. Dalam model tinyspec juga dapat diwarisi dari satu sama lain. Sebagai contoh:
User {name, surname} UserWithPhotos < User {photos: Photo[]} UserForAdmin < User {id, email, lastLoginAt: d}
Postfix dapat bervariasi dan dikombinasikan. Yang utama adalah bahwa nama mereka mencerminkan esensi dan menyederhanakan keakraban dengan dokumentasi.
Pemisahan titik akhir berdasarkan jenis klien
Seringkali titik akhir yang sama mengembalikan data yang berbeda tergantung pada jenis klien atau peran pengguna yang mengakses titik akhir. Misalnya, titik akhir GET /users
dan GET /messages
bisa sangat berbeda untuk pengguna aplikasi seluler Anda dan untuk manajer back office. Pada saat yang sama, mengubah nama titik akhir itu sendiri bisa menjadi terlalu rumit.
Untuk menggambarkan titik akhir yang sama beberapa kali, Anda bisa menambahkan tipenya dalam tanda kurung setelah jalur. Selain itu, berguna untuk menggunakan tag: ini akan membantu untuk membagi dokumentasi titik akhir Anda menjadi grup, yang masing-masing akan dirancang untuk kelompok klien tertentu dari API Anda. Sebagai contoh:
Mobile app: GET /users (mobile) => UserForMobile[] CRM admin panel: GET /users (admin) => UserForAdmin[]
Dokumentasi API SISA
Setelah Anda memiliki spesifikasi dalam format tinyspec atau OpenAPI, Anda dapat menghasilkan dokumentasi yang indah dalam HTML dan menerbitkannya untuk menyenangkan pengembang yang menggunakan API Anda.
Selain layanan cloud yang disebutkan sebelumnya, ada alat CLI yang mengonversi OpenAPI 2.0 ke HTML dan PDF, setelah itu Anda dapat mengunduhnya ke hosting statis apa pun. Contoh:
Apakah Anda tahu lebih banyak contoh? Bagikan di komentar.
Sayangnya, OpenAPI 3.0, dirilis setahun yang lalu, masih kurang didukung, dan saya tidak dapat menemukan contoh dokumentasi yang layak berdasarkan itu: baik di antara solusi cloud, maupun di antara alat CLI. Untuk alasan yang sama, OpenAPI 3.0 belum didukung di tinyspec.
Terbitkan ke GitHub
Salah satu cara termudah untuk menerbitkan dokumentasi adalah GitHub Pages . Cukup aktifkan dukungan halaman statis untuk direktori /docs
di pengaturan repositori Anda dan simpan dokumentasi HTML di folder ini.

Anda dapat menambahkan perintah untuk menghasilkan dokumentasi melalui tinyspec atau alat CLI lain dalam scripts
di package.json
dan memperbarui dokumentasi dengan setiap komit:
"scripts": { "docs": "tinyspec -h -o docs/", "precommit": "npm run docs" }
Integrasi berkelanjutan
Anda dapat memasukkan pembuatan dokumentasi dalam siklus CI dan mempublikasikannya, misalnya, di Amazon S3 di bawah alamat yang berbeda tergantung pada lingkungan atau versi API Anda, misalnya: / /docs/2.0
/docs/stable
/docs/2.0
, /docs/stable
, /docs/staging
.
Awan Tinyspec
Jika Anda menyukai sintaks tinyspec, Anda dapat mendaftar sebagai pengadopsi awal di tinyspec.cloud . Kami akan membangun berdasarkan layanan cloud dan CLI untuk publikasi otomatis dokumentasi dengan beragam pilihan templat dan kemampuan untuk mengembangkan templat kami sendiri.
Kesimpulan
Mengembangkan REST API mungkin merupakan aktivitas paling menyenangkan dari semua yang ada dalam proses bekerja di web modern dan layanan seluler. Tidak ada kebun binatang peramban, sistem operasi dan ukuran layar, semuanya sepenuhnya berada di bawah kendali kami - "di ujung jari Anda".
Mempertahankan spesifikasi dan bonus saat ini dalam bentuk berbagai otomatisasi, yang disediakan secara bersamaan, menjadikan proses ini semakin menyenangkan. API semacam itu menjadi terstruktur, transparan, dan andal.
Sebenarnya, bahkan, jika kita terlibat dalam penciptaan mitos, lalu mengapa kita tidak menjadikannya indah?