Praktik Terbaik untuk Membuat REST API

Halo!



Artikel ini, meskipun judulnya tidak salah, memicu diskusi panjang tentang Stackoverflow sehingga kami tidak dapat mengabaikannya. Upaya untuk memahami besarnya - untuk dengan jelas menceritakan tentang desain yang kompeten dari REST API - tampaknya, penulis berhasil dalam banyak hal, tetapi tidak sepenuhnya. Bagaimanapun, kami berharap dapat bersaing dengan yang asli dalam tingkat diskusi, serta fakta bahwa kami akan bergabung dengan pasukan penggemar Express.



Selamat membaca!



REST API adalah salah satu jenis layanan web paling umum yang tersedia saat ini. Dengan bantuan mereka, berbagai klien, termasuk aplikasi browser, dapat bertukar informasi dengan server melalui REST API.



Oleh karena itu, sangat penting untuk mendesain REST API dengan benar sehingga Anda tidak mengalami masalah apa pun. Pertimbangkan keamanan, kinerja, dan kegunaan API dari perspektif konsumen.



Jika tidak, kami akan menimbulkan masalah bagi pelanggan yang menggunakan API kami - yang membuat frustrasi dan mengganggu. Jika kita tidak mengikuti konvensi umum, maka kita hanya akan membingungkan mereka yang akan memelihara API kita, serta pelanggan, karena arsitekturnya akan berbeda dari yang diharapkan semua orang.



Artikel ini akan membahas cara mendesain REST API sedemikian rupa sehingga sederhana dan dapat dipahami oleh semua orang yang mengonsumsinya. Kami akan memastikan daya tahan, keamanan, dan kecepatan mereka, karena data yang dikirimkan ke klien melalui API semacam itu dapat dirahasiakan.



Karena ada banyak alasan dan opsi untuk aplikasi jaringan gagal, kita harus memastikan bahwa kesalahan dalam REST API apa pun ditangani dengan baik dan disertai dengan kode HTTP standar untuk membantu konsumen mengatasi masalah tersebut.



Terima JSON dan kembalikan JSON sebagai tanggapan



REST API harus menerima JSON untuk payload permintaan dan juga mengirim tanggapan JSON. JSON adalah standar transfer data. Hampir semua teknologi jaringan diadaptasi untuk menggunakannya: JavaScript memiliki metode bawaan untuk encoding dan decoding JSON, baik melalui Fetch API atau melalui klien HTTP lain. Teknologi sisi server menggunakan pustaka untuk mendekode JSON dengan sedikit atau tanpa campur tangan Anda.



Ada cara lain untuk mentransfer data. XML itu sendiri tidak didukung secara luas dalam kerangka kerja; biasanya Anda perlu mengonversi data ke format yang lebih nyaman, yang biasanya JSON. Di sisi klien, terutama di browser, tidak mudah menangani data ini. Anda harus melakukan banyak pekerjaan ekstra hanya untuk memastikan transfer data normal.



Formulir memang nyaman untuk mentransfer data, terutama jika kita akan mentransfer file. Tetapi untuk mentransfer informasi dalam bentuk teks dan numerik, Anda dapat melakukannya tanpa formulir, karena sebagian besar kerangka kerja memungkinkan JSON untuk dikirim tanpa pemrosesan tambahan - cukup ambil data di sisi klien. Ini adalah cara paling mudah untuk mengatasinya.



Untuk memastikan bahwa klien menafsirkan JSON yang diterima dari REST API kami persis seperti JSON, Content-Typeheader respons harus disetel ke nilai application/jsonsetelah permintaan dibuat. Banyak kerangka kerja aplikasi sisi server menyetel header respons secara otomatis. Beberapa klien HTTP melihat Content-Typeheader respons dan mengurai data sesuai dengan format yang ditentukan di sana.



Satu-satunya pengecualian terjadi saat kami mencoba mengirim dan menerima file yang ditransfer antara klien dan server. Kemudian Anda perlu memproses file yang diterima sebagai tanggapan dan mengirim data formulir dari klien ke server. Tapi ini topik untuk artikel lain.



Kami juga perlu memastikan bahwa JSON adalah respons dari titik akhir kami. Banyak kerangka kerja server memiliki fitur ini bawaan.



Mari kita ambil contoh API yang menerima payload JSON. Contoh ini menggunakan kerangka kerja backend Ekspres untuk Node.js. Kita bisa menggunakan program sebagai middleware body-parseruntuk mengurai isi permintaan JSON, lalu memanggil metode res.jsondengan objek yang ingin kita kembalikan sebagai respons JSON. Ini dilakukan seperti ini:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.post('/', (req, res) => {
  res.json(req.body);
});

app.listen(3000, () => console.log('server started'));


bodyParser.json()mengurai string tubuh permintaan menjadi JSON, mengonversinya menjadi objek JavaScript, dan kemudian menetapkan hasilnya ke objek req.body.



Setel header Tipe Konten dalam respons ke nilai application/json; charset=utf-8tanpa perubahan apa pun. Metode yang ditunjukkan di atas berlaku untuk sebagian besar kerangka kerja backend lainnya.



Kami menggunakan nama untuk jalur ke titik akhir, bukan kata kerja



Nama jalur ke titik akhir tidak boleh berupa kata kerja, tetapi nama. Nama ini mewakili objek dari titik akhir yang kita ambil dari sana, atau yang kita manipulasi.



Intinya adalah nama metode permintaan HTTP kita sudah berisi kata kerja. Menempatkan kata kerja di nama jalur ke titik akhir API tidak praktis; selain itu, nama tersebut ternyata panjangnya tidak perlu dan tidak membawa informasi yang berharga. Kata kerja yang dipilih oleh pengembang dapat dibuat sederhana tergantung pada keinginannya. Misalnya, beberapa orang lebih suka opsi 'dapatkan', dan beberapa lebih memilih 'ambil', jadi lebih baik Anda membatasi diri Anda pada kata kerja HTTP GET yang memberi tahu Anda apa yang dilakukan titik akhir.



Tindakan tersebut harus ditentukan dalam nama metode HTTP dari permintaan yang kita buat. Metode yang paling umum berisi kata kerja GET, POST, PUT, dan DELETE.

GET mengambil sumber daya. POST mengirimkan data baru ke server. PUT memperbarui data yang ada. HAPUS menghapus data. Masing-masing kata kerja ini sesuai dengan salah satu operasi dari grup CRUD .



Mempertimbangkan dua prinsip yang dibahas di atas, untuk menerima artikel baru, kita harus membuat rute dalam bentuk GET /articles/. Demikian pula, kami menggunakan POST /articles/untuk memperbarui artikel baru, PUT /articles/:id untuk memperbarui artikel dengan artikel yang diberikan id. Metode DELETE /articles/:iddirancang untuk menghapus artikel dengan ID tertentu.



/articlesAdalah resource REST API. Misalnya, Anda dapat menggunakan Express untuk melakukan hal berikut dengan artikel:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles', (req, res) => {
  const articles = [];
  //    ...
  res.json(articles);
});

app.post('/articles', (req, res) => {
  //     ...
  res.json(req.body);
});

app.put('/articles/:id', (req, res) => {
  const { id } = req.params;
  //    ...
  res.json(req.body);
});

app.delete('/articles/:id', (req, res) => {
  const { id } = req.params;
  //    ...
  res.json({ deleted: id });
});

app.listen(3000, () => console.log('server started'));


Dalam kode di atas, kami telah menentukan titik akhir untuk memanipulasi artikel. Seperti yang Anda lihat, tidak ada kata kerja di nama jalur. Nama saja. Kata kerja hanya digunakan dalam nama metode HTTP.



Titik akhir POST, PUT, dan DELETE menerima badan permintaan JSON dan mengembalikan respons JSON juga, termasuk titik akhir GET.



Koleksi disebut kata benda jamak



Koleksi harus diberi nama dengan kata benda jamak. Jarang sekali kita hanya perlu mengambil satu item dari sebuah koleksi, jadi kita harus konsisten dan menggunakan kata benda jamak dalam nama koleksi.



Bentuk jamak juga digunakan untuk konsistensi dengan konvensi penamaan dalam database. Biasanya, tabel berisi tidak hanya satu, tetapi banyak catatan, dan tabel diberi nama yang sesuai.



Saat bekerja dengan titik akhir, /articleskami menggunakan jamak saat menamai semua titik akhir.



Sumber daya bersarang saat bekerja dengan objek hierarki



Jalur titik akhir yang berhubungan dengan sumber daya bertingkat harus memiliki struktur seperti ini: tambahkan sumber daya bertingkat sebagai nama jalur setelah nama sumber daya induk.

Anda perlu memastikan bahwa penumpukan sumber daya dalam kode persis sama dengan penumpukan informasi di tabel database kita. Jika tidak, kebingungan mungkin terjadi.



Misalnya, jika kita ingin menerima komentar tentang artikel baru di titik akhir tertentu, kita harus melampirkan jalur / komentar di ujung jalan /articles. Dalam hal ini, diasumsikan bahwa kami menganggap entitas komentar sebagai entitas anak articledalam database kami.



Misalnya, Anda dapat melakukan ini dengan kode berikut di Express:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.json());

app.get('/articles/:articleId/comments', (req, res) => {
  const { articleId } = req.params;
  const comments = [];
  //      articleId
  res.json(comments);
});


app.listen(3000, () => console.log('server started'));


Dalam kode di atas, Anda dapat menggunakan metode GET di jalur '/articles/:articleId/comments'. Kami menerima komentar commentspada artikel yang cocok articleId, dan kemudian mengembalikannya sebagai tanggapan. Kami menambahkan 'comments'setelah segmen jalur '/articles/:articleId'untuk menunjukkan bahwa ini adalah sumber daya anak /articles.



Ini masuk akal karena komentar adalah objek turunan articlesdan diasumsikan bahwa setiap artikel memiliki kumpulan komentarnya sendiri. Jika tidak, struktur ini dapat membingungkan pengguna, karena biasanya digunakan untuk mengakses objek turunan. Prinsip yang sama berlaku saat bekerja dengan titik akhir POST, PUT, dan DELETE. Mereka semua menggunakan struktur bersarang yang sama saat membuat nama jalur.



Penanganan kesalahan yang rapi dan mengembalikan kode kesalahan standar



Untuk menghindari kebingungan saat terjadi kesalahan pada API, Anda perlu menangani kesalahan dengan hati-hati dan mengembalikan kode tanggapan HTTP yang menunjukkan kesalahan mana yang terjadi. Ini memberi pengelola API informasi yang cukup untuk memahami masalahnya. Tidak dapat diterima jika kesalahan merusak sistem, oleh karena itu kesalahan tidak dapat dibiarkan tanpa pemrosesan, dan konsumen API harus menangani pemrosesan tersebut.



Kode kesalahan HTTP yang paling umum adalah:



  • 400 Permintaan Buruk - Menunjukkan bahwa input yang diterima dari klien gagal validasi.
  • 401 Unauthorized - artinya pengguna belum login dan oleh karena itu tidak memiliki izin untuk mengakses sumber daya. Biasanya, kode ini dikeluarkan saat pengguna tidak diautentikasi.
  • 403 Forbidden - Menunjukkan bahwa pengguna diautentikasi tetapi tidak berwenang untuk mengakses sumber daya.
  • 404 Not Found - berarti sumber daya tidak ditemukan
  • 500 Kesalahan server internal adalah kesalahan server dan mungkin tidak boleh ditampilkan secara eksplisit.
  • 502 Bad Gateway - Menunjukkan pesan balasan yang tidak valid dari server upstream.
  • 503 Service Unavailable - berarti sesuatu yang tidak terduga terjadi di sisi server - misalnya, server kelebihan beban, kegagalan beberapa elemen sistem, dll.


Anda harus mengeluarkan kode yang sesuai dengan kesalahan yang mencegah aplikasi kita. Misalnya, jika kita ingin menolak data yang diterima sebagai payload permintaan, maka sesuai dengan aturan Express API, kita harus mengembalikan kode 400:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

//  
const users = [
  { email: 'abc@foo.com' }
]

app.use(bodyParser.json());

app.post('/users', (req, res) => {
  const { email } = req.body;
  const userExists = users.find(u => u.email === email);
  if (userExists) {
    return res.status(400).json({ error: 'User already exists' })
  }
  res.json(req.body);
});


app.listen(3000, () => console.log('server started'));


Dalam kode di atas, kami memegang dalam array pengguna daftar pengguna yang sudah ada yang mengetahui email.



Selanjutnya, jika kami mencoba mengirim payload dengan nilai yang emailsudah ada di pengguna, kami mendapat respons dengan kode 400 dan pesan yang 'User already exists'menunjukkan bahwa pengguna tersebut sudah ada. Dengan informasi ini, pengguna bisa menjadi lebih baik - mengganti alamat email dengan yang belum ada dalam daftar.



Kode kesalahan harus selalu disertai dengan pesan yang cukup informatif untuk menghilangkan kesalahan, tetapi tidak terlalu mendetail sehingga informasi ini dapat digunakan oleh penyerang yang berniat untuk mencuri informasi kami atau merusak sistem.



Setiap kali API kami gagal dimatikan dengan benar, kami harus menangani kegagalan dengan hati-hati dengan mengirimkan informasi kesalahan untuk memudahkan pengguna memperbaiki situasi.



Izinkan pengurutan, pemfilteran, dan penomoran halaman data



Basis di balik REST API bisa berkembang pesat. Terkadang ada begitu banyak data sehingga tidak mungkin mendapatkan semuanya kembali sekaligus, karena ini akan memperlambat sistem atau bahkan menurunkannya. Oleh karena itu, diperlukan cara untuk memfilter item.



Kami juga membutuhkan cara untuk memberi nomor pada data sehingga kami hanya mengembalikan beberapa hasil pada satu waktu. Kami tidak ingin mengambil terlalu lama sumber daya mencoba menarik semua data yang diminta sekaligus.



Baik pemfilteran dan paginasi data dapat meningkatkan kinerja dengan mengurangi penggunaan sumber daya server. Semakin banyak data terakumulasi dalam database, semakin penting kedua kemungkinan ini.



Berikut adalah contoh kecil di mana API dapat menerima string kueri dengan berbagai parameter. Mari memfilter item berdasarkan bidangnya:



const express = require('express');
const bodyParser = require('body-parser');

const app = express();

//      
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  const { firstName, lastName, age } = req.query;
  let results = [...employees];
  if (firstName) {
    results = results.filter(r => r.firstName === firstName);
  }

  if (lastName) {
    results = results.filter(r => r.lastName === lastName);
  }

  if (age) {
    results = results.filter(r => +r.age === +age);
  }
  res.json(results);
});

app.listen(3000, () => console.log('server started'));


Pada kode di atas, kita memiliki variabel req.queryyang memungkinkan kita mendapatkan parameter permintaan. Kami kemudian dapat mengekstrak nilai properti dengan merusak parameter kueri individu menjadi variabel; JavaScript memiliki sintaks khusus untuk ini.



Terakhir, kami menerapkan filter pada setiap nilai parameter kueri untuk menemukan item yang ingin kami kembalikan.



Setelah ini selesai, kami mengembalikan hasil sebagai tanggapan. Karenanya, saat melakukan permintaan GET ke jalur berikut dengan string kueri:



/employees?lastName=Smith&age=30


Kita mendapatkan:



[
    {
        "firstName": "John",
        "lastName": "Smith",
        "age": 30
    }
]


sebagai tanggapan yang dikembalikan karena pemfilteran aktif lastNamedan age.



Demikian pula, Anda dapat menerima parameter kueri laman dan mengembalikan sekelompok rekaman yang menempati posisi dari (page - 1) * 20hingga page * 20.



Juga dalam string kueri, Anda dapat menentukan bidang yang akan digunakan untuk menyortir. Dalam hal ini, kita dapat mengurutkannya berdasarkan bidang terpisah ini. Misalnya, kita mungkin perlu mengekstrak string kueri dari URL seperti ini:



http://example.com/articles?sort=+author,-datepublished


Dimana +artinya "naik" dan "turun". Jadi, kami mengurutkan menurut nama penulis menurut abjad dan menurut tanggal diterbitkan dari terbaru ke terlama.



Patuhi praktik keamanan yang terbukti



Komunikasi antara klien dan server sebagian besar harus bersifat pribadi, karena kami sering mengirim dan menerima informasi rahasia. Karenanya, menggunakan SSL / TLS untuk keamanan adalah suatu keharusan.



Sertifikat SSL tidak terlalu sulit untuk diunggah ke server, dan sertifikat itu sendiri gratis atau sangat murah. Tidak ada alasan untuk menolak mengizinkan REST API kami berkomunikasi melalui saluran aman daripada saluran terbuka.



Seseorang seharusnya tidak diberi akses ke informasi lebih dari yang dia minta. Misalnya, pengguna biasa tidak boleh mendapatkan akses ke informasi pengguna lain. Selain itu, dia seharusnya tidak dapat melihat data administrator.



Untuk mempromosikan prinsip hak istimewa terendah, Anda harus menerapkan pemeriksaan peran untuk peran tertentu, atau memberikan lebih banyak perincian peran untuk setiap pengguna.



Jika kami memutuskan untuk mengelompokkan pengguna ke dalam beberapa peran, maka peran tersebut harus dilengkapi dengan hak akses yang memastikan bahwa semua yang dibutuhkan pengguna telah selesai dan tidak lebih. Jika kami menetapkan secara lebih rinci hak akses untuk setiap peluang yang diberikan kepada pengguna, maka kami perlu memastikan bahwa administrator dapat memberikan kemampuan ini kepada pengguna mana pun, atau menghilangkan kemampuan ini. Selain itu, Anda perlu menambahkan beberapa peran yang telah ditentukan sebelumnya yang dapat diterapkan ke grup pengguna sehingga Anda tidak perlu menyetel hak yang diperlukan untuk setiap pengguna secara manual.



Cache data untuk meningkatkan kinerja



Caching dapat ditambahkan untuk mengembalikan data dari cache memori lokal, daripada mengambil beberapa data dari database setiap kali pengguna memintanya. Keuntungan dari caching adalah pengguna dapat mengambil data lebih cepat. Namun, data ini mungkin sudah usang. Ini juga bisa penuh dengan masalah saat men-debug di lingkungan produksi, ketika terjadi kesalahan, dan kami terus melihat data lama.



Ada berbagai opsi cache yang tersedia, seperti Redis , cache dalam memori, dan banyak lagi. Anda dapat mengubah cara data di-cache sesuai kebutuhan.



Misalnya, Express menyediakan middlewareapicacheuntuk menambahkan kemampuan caching ke aplikasi Anda tanpa konfigurasi yang rumit. Caching dalam memori sederhana dapat ditambahkan ke server seperti ini:



const express = require('express');

const bodyParser = require('body-parser');
const apicache = require('apicache');
const app = express();
let cache = apicache.middleware;
app.use(cache('5 minutes'));

//      
const employees = [
  { firstName: 'Jane', lastName: 'Smith', age: 20 },
  //...
  { firstName: 'John', lastName: 'Smith', age: 30 },
  { firstName: 'Mary', lastName: 'Green', age: 50 },
]

app.use(bodyParser.json());

app.get('/employees', (req, res) => {
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


Kode di atas hanya mengacu apicachedengan apicache.middleware, menghasilkan:



app.use(cache('5 minutes'))


dan itu cukup untuk menerapkan cache di seluruh aplikasi. Kami menyimpan, misalnya, semua hasil dalam lima menit. Selanjutnya nilai ini bisa kita sesuaikan dengan kebutuhan kita.



Pembuatan versi API



Kita harus memiliki versi API yang berbeda jika kita membuat perubahan pada mereka yang mungkin mengganggu klien. Pembuatan versi dapat dilakukan secara semantik (misalnya, 2.0.6 berarti versi mayor adalah 2, dan ini adalah tambalan keenam). Prinsip ini sekarang diterima di sebagian besar aplikasi.



Dengan cara ini Anda dapat secara bertahap menghentikan endpoint lama daripada memaksa semua orang untuk beralih ke API baru secara bersamaan. Anda dapat menyimpan versi v1 bagi mereka yang tidak ingin mengubah apa pun, dan menyediakan versi v2 dengan semua fitur barunya bagi mereka yang siap untuk meningkatkan. Ini sangat penting dalam konteks API publik. Mereka perlu diversi agar tidak merusak aplikasi pihak ketiga yang menggunakan API kami.



Pembuatan versi biasanya dilakukan dengan menambahkan /v1/,/v2/, dll., ditambahkan di awal jalur API.



Misalnya, berikut cara melakukannya di Express:



const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());

app.get('/v1/employees', (req, res) => {
  const employees = [];
  //      
  res.json(employees);
});

app.get('/v2/employees', (req, res) => {
  const employees = [];
  //       
  res.json(employees);
});

app.listen(3000, () => console.log('server started'));


Kami hanya menambahkan nomor versi ke awal jalan menuju ke titik akhir.



Kesimpulan



Hal terpenting dari mendesain REST API berkualitas tinggi adalah menjaga konsistensi dengan mengikuti standar dan konvensi web. Kode Status JSON, SSL / TLS, dan HTTP harus dimiliki di web saat ini.



Performa sama pentingnya. Anda dapat meningkatkannya tanpa mengembalikan terlalu banyak data sekaligus. Selain itu, Anda dapat menggunakan caching untuk menghindari permintaan data yang sama berulang kali.



Jalur titik akhir harus diberi nama secara konsisten. Anda harus menggunakan kata benda dalam namanya, karena kata kerja ada dalam nama metode HTTP. Jalur sumber daya bertingkat harus mengikuti jalur sumber daya induk. Mereka harus mengkomunikasikan apa yang kami terima atau manipulasi, sehingga kami tidak perlu membaca dokumentasi tambahan untuk memahami apa yang terjadi.



All Articles