Membangun REST API Node.js/TypeScript, Bagian 2: Model, Middleware, dan Layanan

Diterbitkan: 2022-03-11

Dalam artikel pertama seri REST API kami, kami membahas cara menggunakan npm untuk membuat back end dari awal, menambahkan dependensi seperti TypeScript, menggunakan modul debug yang dibangun ke dalam Node.js, membangun struktur proyek Express.js, dan mencatat runtime acara secara fleksibel dengan Winston. Jika Anda sudah nyaman dengan konsep-konsep itu, cukup klon ini, alihkan ke cabang toptal-article-01 dengan git checkout , dan baca terus.

Layanan REST API, Middleware, Pengontrol, dan Model

Seperti yang dijanjikan, sekarang kita akan masuk ke detail tentang modul ini:

  • Layanan yang membuat kode kami lebih bersih dengan mengenkapsulasi operasi logika bisnis ke dalam fungsi yang dapat dipanggil oleh middleware dan pengontrol.
  • Middleware yang akan memvalidasi kondisi prasyarat sebelum Express.js memanggil fungsi pengontrol yang sesuai.
  • Pengendali yang menggunakan layanan untuk memproses permintaan sebelum akhirnya mengirimkan respons kepada pemohon.
  • Model yang menjelaskan data kami dan membantu dalam pemeriksaan waktu kompilasi.

Kami juga akan menambahkan database yang sangat sederhana yang sama sekali tidak cocok untuk produksi. (Satu-satunya tujuannya adalah untuk membuat tutorial ini lebih mudah diikuti, membuka jalan bagi artikel kami berikutnya untuk mempelajari koneksi database dan integrasi dengan MongoDB dan Mongoose.)

Praktis: Langkah Pertama dengan DAO, DTO, dan Database Sementara Kami

Untuk bagian tutorial ini, database kami bahkan tidak akan menggunakan file. Ini hanya akan menyimpan data pengguna dalam array, yang berarti data akan menguap setiap kali kita keluar dari Node.js. Ini hanya akan mendukung operasi membuat, membaca, memperbarui, dan menghapus (CRUD) paling dasar.

Kami akan menggunakan dua konsep di sini:

  • Objek akses data (DAO)
  • Objek transfer data (DTO)

Perbedaan satu huruf antara akronim itu penting: DAO bertanggung jawab untuk menghubungkan ke database yang ditentukan dan melakukan operasi CRUD; DTO adalah objek yang menyimpan data mentah yang DAO akan kirim ke—dan terima dari—database.

Dengan kata lain, DTO adalah objek yang sesuai dengan tipe model data, dan DAO adalah layanan yang menggunakannya.

Meskipun DTO bisa menjadi lebih rumit—mewakili entitas database bersarang, misalnya—dalam artikel ini, satu instans DTO akan sesuai dengan tindakan tertentu pada satu baris database.

Mengapa DTO?

Menggunakan DTO agar objek TypeScript kami sesuai dengan model data kami membantu menjaga konsistensi arsitektur, seperti yang akan kita lihat di bagian layanan di bawah ini. Tapi ada peringatan penting: Baik DTO maupun TypeScript sendiri tidak menjanjikan validasi input pengguna otomatis apa pun, karena itu harus terjadi saat runtime. Saat kode kami menerima input pengguna pada titik akhir di API kami, input tersebut dapat:

  • Memiliki bidang ekstra
  • Tidak ada bidang wajib yang harus diisi (yaitu, yang tidak diberi akhiran ? )
  • Memiliki bidang di mana datanya bukan tipe yang kami tentukan dalam model kami menggunakan TypeScript

TypeScript (dan JavaScript yang ditranspilasikannya) tidak akan secara ajaib memeriksa ini untuk kami, jadi penting untuk tidak melupakan validasi ini, terutama saat membuka API Anda ke publik. Paket seperti ajv dapat membantu dengan ini tetapi biasanya bekerja dengan mendefinisikan model dalam objek skema khusus perpustakaan daripada TypeScript asli. (Luwak, yang dibahas dalam artikel berikutnya, akan memainkan peran serupa dalam proyek ini.)

Anda mungkin berpikir, “Apakah yang terbaik adalah menggunakan DAO dan DTO, daripada menggunakan yang lebih sederhana?” Pengembang perusahaan Gunther Popp menawarkan jawaban; Anda sebaiknya menghindari DTO di sebagian besar proyek Express.js/TypeScript di dunia nyata yang lebih kecil kecuali jika Anda dapat berharap untuk menskalakan secara wajar dalam jangka menengah.

Tetapi bahkan jika Anda tidak akan menggunakannya dalam produksi, proyek contoh ini adalah kesempatan berharga untuk menguasai arsitektur TypeScript API. Ini adalah cara yang bagus untuk berlatih memanfaatkan jenis TypeScript dengan cara tambahan dan bekerja dengan DTO untuk melihat bagaimana mereka dibandingkan dengan pendekatan yang lebih mendasar saat menambahkan komponen dan model.

Model API REST Pengguna kami di Level TypeScript

Pertama kita akan mendefinisikan tiga DTO untuk pengguna kita. Mari buat folder bernama dto di dalam folder users , dan buat file di sana bernama create.user.dto.ts yang berisi berikut ini:

 export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }

Kami mengatakan bahwa setiap kali kami membuat pengguna, terlepas dari basis datanya, ia harus memiliki ID, kata sandi, dan email, dan secara opsional nama depan dan belakang. Persyaratan ini dapat berubah berdasarkan persyaratan bisnis dari proyek tertentu.

Untuk permintaan PUT , kami ingin memperbarui seluruh objek, jadi bidang opsional kami sekarang diperlukan. Di folder yang sama, buat file bernama put.user.dto.ts dengan kode ini:

 export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; }

Untuk permintaan PATCH , kita dapat menggunakan fitur Partial dari TypeScript, yang membuat tipe baru dengan menyalin tipe lain dan menjadikan semua bidangnya opsional. Dengan begitu, file patch.user.dto.ts hanya perlu berisi kode berikut:

 import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {}

Sekarang, mari kita buat database sementara di dalam memori. Mari buat folder bernama daos di dalam folder users , dan tambahkan file bernama users.dao.ts .

Pertama, kami ingin mengimpor DTO yang kami buat:

 import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';

Sekarang, untuk menangani ID pengguna kita, mari tambahkan pustaka shortid (menggunakan terminal):

 npm i shortid npm i --save-dev @types/shortid

Kembali ke users.dao.ts , kami akan mengimpor shortid:

 import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao');

Kita sekarang dapat membuat kelas yang disebut UsersDao , yang akan terlihat seperti ini:

 class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao();

Dengan menggunakan pola tunggal, kelas ini akan selalu menyediakan instance yang sama—dan, yang terpenting, array users yang sama—ketika kita mengimpornya ke file lain. Itu karena Node.js men-cache file ini di mana pun itu diimpor, dan semua impor terjadi saat startup. Artinya, file apa pun yang merujuk ke users.dao.ts akan diberikan referensi ke new UsersDao() yang sama yang diekspor saat pertama kali Node.js memproses file ini.

Kita akan melihat ini berfungsi ketika kita menggunakan kelas ini lebih lanjut dalam artikel ini, dan menggunakan pola TypeScript/Express.js umum ini untuk sebagian besar kelas di seluruh proyek.

Catatan: Kerugian yang sering dikutip untuk lajang adalah bahwa mereka sulit untuk menulis unit test. Dalam kasus banyak kelas kami, kerugian ini tidak akan berlaku, karena tidak ada variabel anggota kelas yang perlu diatur ulang. Tetapi bagi mereka yang menginginkannya, kami membiarkannya sebagai latihan bagi pembaca untuk mempertimbangkan pendekatan masalah ini dengan penggunaan injeksi ketergantungan.

Sekarang kita akan menambahkan operasi CRUD dasar ke kelas sebagai fungsi. Fungsi create akan terlihat seperti ini:

 async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }

Baca akan datang dalam dua rasa, "baca semua sumber daya" dan "baca satu per ID." Mereka dikodekan seperti ini:

 async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); }

Demikian juga, pembaruan berarti menimpa objek lengkap (sebagai PUT ) atau hanya sebagian objek (sebagai PATCH ):

 async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; }

Seperti disebutkan sebelumnya, terlepas dari deklarasi UserDto kami dalam tanda tangan fungsi ini, TypeScript tidak menyediakan pemeriksaan jenis runtime. Ini berarti bahwa:

  • putUserById() memiliki bug. Ini akan memungkinkan konsumen API menyimpan nilai untuk bidang yang bukan bagian dari model yang ditentukan oleh DTO kami.
  • patchUserById() bergantung pada daftar duplikat nama bidang yang harus tetap sinkron dengan model. Tanpa ini, itu harus menggunakan objek yang diperbarui untuk daftar ini. Itu berarti itu akan secara diam-diam mengabaikan nilai untuk bidang yang merupakan bagian dari model yang ditentukan DTO tetapi belum pernah disimpan sebelumnya untuk contoh objek khusus ini.

Namun kedua skenario ini akan ditangani dengan benar di tingkat database di artikel berikutnya.

Operasi terakhir, untuk menghapus sumber daya, akan terlihat seperti ini:

 async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }

Sebagai bonus, mengetahui bahwa prasyarat untuk membuat pengguna adalah untuk memvalidasi jika email pengguna tidak diduplikasi, mari tambahkan fungsi "dapatkan pengguna melalui email" sekarang:

 async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } }

Catatan: Dalam skenario dunia nyata, Anda mungkin akan terhubung ke database menggunakan pustaka yang sudah ada sebelumnya, seperti Mongoose atau Sequelize, yang akan mengabstraksi semua operasi dasar yang mungkin Anda perlukan. Karena itu, kami tidak akan membahas detail fungsi yang diterapkan di atas.

Lapisan Layanan REST API kami

Sekarang kita memiliki DAO dasar dalam memori, kita dapat membuat layanan yang akan memanggil fungsi CRUD. Karena fungsi CRUD adalah sesuatu yang harus dimiliki oleh setiap layanan yang akan terhubung ke database, kita akan membuat antarmuka CRUD yang berisi metode yang ingin kita terapkan setiap kali kita ingin mengimplementasikan layanan baru.

Saat ini, IDE yang kami gunakan memiliki fitur pembuatan kode untuk menambahkan fungsi yang kami implementasikan, mengurangi jumlah kode berulang yang perlu kami tulis.

Contoh cepat menggunakan IDE WebStorm:

Tangkapan layar WebStorm menunjukkan definisi kosong untuk kelas bernama MyService yang mengimplementasikan antarmuka yang disebut CRUD. Nama MyService digarisbawahi dengan warna merah oleh IDE.

IDE menyoroti nama kelas MyService dan menyarankan opsi berikut:

Tangkapan layar yang mirip dengan yang sebelumnya, tetapi dengan menu konteks yang mencantumkan beberapa opsi, yang pertama adalah "Implement all members."

Opsi "Implementasikan semua anggota" secara instan membuat perancah fungsi yang diperlukan agar sesuai dengan antarmuka CRUD :

Tangkapan layar kelas MyService di WebStorm. MyService tidak lagi digarisbawahi dengan warna merah, dan definisi kelas sekarang berisi semua tanda tangan fungsi yang diketik TypeScript (bersama dengan badan fungsi, baik kosong atau berisi pernyataan kembali) yang ditentukan dalam antarmuka CRUD.

Itu semua dikatakan, pertama-tama mari kita buat antarmuka TypeScript kita, yang disebut CRUD . Di folder common kita, mari buat folder bernama interfaces dan tambahkan crud.interface.ts dengan yang berikut:

 export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; }

Setelah itu selesai, mari buat folder services di dalam folder users dan tambahkan file users.service.ts di sana, yang berisi:

 import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService();

Langkah pertama kami di sini adalah mengimpor DAO dalam memori kami, ketergantungan antarmuka kami, dan jenis TypeScript dari masing-masing DTO kami, saatnya untuk mengimplementasikan UsersService sebagai layanan tunggal, pola yang sama yang kami gunakan dengan DAO kami.

Semua fungsi CRUD sekarang hanya memanggil fungsi masing-masing UsersDao . Ketika tiba saatnya untuk mengganti DAO, kita tidak perlu membuat perubahan di tempat lain dalam proyek, kecuali untuk beberapa penyesuaian pada file ini di mana fungsi DAO dipanggil, seperti yang akan kita lihat di Bagian 3.

Misalnya, kita tidak perlu melacak setiap panggilan ke list() dan memeriksa konteksnya sebelum menggantinya. Itulah keuntungan memiliki lapisan pemisahan ini, dengan mengorbankan sejumlah kecil pelat ketel awal yang Anda lihat di atas.

Async/Menunggu dan Node.js

Penggunaan async kami untuk fungsi layanan mungkin tampak sia-sia. Untuk saat ini, ini adalah: Semua fungsi ini segera mengembalikan nilainya, tanpa penggunaan internal Promise s atau await . Ini semata-mata untuk mempersiapkan basis kode kami untuk layanan yang akan menggunakan async . Demikian juga, di bawah, Anda akan melihat bahwa semua panggilan ke fungsi ini menggunakan await .

Pada akhir artikel ini, Anda akan kembali memiliki proyek yang dapat dijalankan untuk bereksperimen. Itu akan menjadi saat yang tepat untuk mencoba menambahkan berbagai jenis kesalahan di tempat yang berbeda dalam basis kode, dan melihat apa yang terjadi selama kompilasi dan pengujian. Kesalahan dalam konteks async khususnya mungkin tidak berperilaku seperti yang Anda harapkan. Ada baiknya menggali dan mengeksplorasi berbagai solusi, yang berada di luar cakupan artikel ini.


Sekarang, setelah DAO dan layanan kami siap, mari kembali ke pengontrol pengguna.

Membangun Pengontrol API REST Kami

Seperti yang kami katakan di atas, ide di balik pengontrol adalah untuk memisahkan konfigurasi rute dari kode yang akhirnya memproses permintaan rute. Itu berarti bahwa semua validasi harus dilakukan sebelum permintaan kita mencapai controller. Pengendali hanya perlu tahu apa yang harus dilakukan dengan permintaan yang sebenarnya karena jika permintaan dibuat sejauh itu, maka kita tahu itu ternyata valid. Pengontrol kemudian akan memanggil layanan masing-masing dari setiap permintaan yang akan ditanganinya.

Sebelum memulai, kita perlu menginstal pustaka untuk hashing kata sandi pengguna dengan aman:

 npm i argon2

Mari kita mulai dengan membuat folder bernama controllers di dalam folder users controller dan membuat file bernama users.controller.ts di dalamnya:

 // we import express to add types to the request/response objects from our controller functions import express from 'express'; // we import our newly created user services import usersService from '../services/users.service'; // we import the argon2 library for password hashing import argon2 from 'argon2'; // we use debug with a custom context as described in Part 1 import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController();

Catatan: Baris di atas tidak mengirimkan kembali apa pun dengan HTTP 204 No Content sejalan dengan RFC 7231 pada topik.

Dengan singleton pengontrol pengguna kami selesai, kami siap untuk mengkodekan modul lain yang bergantung pada contoh model dan layanan objek REST API kami: middleware pengguna kami.

Node.js REST Middleware dengan Express.js

Apa yang dapat kita lakukan dengan middleware Express.js? Validasi sangat cocok, salah satunya. Mari tambahkan beberapa validasi dasar untuk bertindak sebagai penjaga gerbang untuk permintaan sebelum mereka membuatnya ke pengontrol pengguna kita:

  • Pastikan keberadaan bidang pengguna seperti email dan password yang diperlukan untuk membuat atau memperbarui pengguna
  • Pastikan email yang diberikan belum digunakan
  • Periksa apakah kami tidak mengubah bidang email setelah pembuatan (karena kami menggunakannya sebagai ID utama yang menghadap pengguna untuk kesederhanaan)
  • Validasi apakah pengguna tertentu ada

Agar validasi ini berfungsi dengan Express.js, kita perlu menerjemahkannya ke dalam fungsi yang mengikuti pola kontrol aliran Express.js menggunakan next() , seperti yang dijelaskan dalam artikel sebelumnya. Kami membutuhkan file baru, users/middleware/users.middleware.ts :

 import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware();

Dengan keluarnya boilerplate singleton yang sudah dikenal, mari tambahkan beberapa fungsi middleware ke badan kelas:

 async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: `Missing required fields email and password`, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: `User email already exists` }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: `Invalid email` }); } } // Here we need to use an arrow function to bind `this` correctly validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: `User ${req.params.userId} not found`, }); } }

Untuk mempermudah konsumen API kami membuat permintaan lebih lanjut tentang pengguna yang baru ditambahkan, kami akan menambahkan fungsi pembantu yang akan mengekstrak userId dari parameter permintaan—yang masuk dari URL permintaan itu sendiri—dan menambahkannya ke badan permintaan, tempat data pengguna lainnya berada.

Idenya di sini adalah untuk dapat dengan mudah menggunakan permintaan seluruh tubuh ketika kami ingin memperbarui informasi pengguna, tanpa khawatir mendapatkan ID dari parameter setiap saat. Sebaliknya, itu diurus hanya di satu tempat, middleware. Fungsinya akan terlihat seperti ini:

 async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); }

Selain logika, perbedaan utama antara middleware dan pengontrol adalah bahwa sekarang kita menggunakan fungsi next() untuk meneruskan kontrol di sepanjang rantai fungsi yang dikonfigurasi hingga tiba di tujuan akhir, yang dalam kasus kita adalah pengontrol.

Menyatukan Semuanya: Memfaktorkan Ulang Rute Kami

Sekarang kita telah mengimplementasikan semua aspek baru dari arsitektur proyek kita, mari kembali ke file users.routes.config.ts yang kita definisikan di artikel sebelumnya. Ini akan memanggil middleware dan pengontrol kami, yang keduanya bergantung pada layanan pengguna kami, yang pada gilirannya membutuhkan model pengguna kami.

File terakhir akan sesederhana ini:

 import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } }

Di sini, kami telah mendefinisikan ulang rute kami dengan menambahkan middleware untuk memvalidasi logika bisnis kami dan fungsi pengontrol yang sesuai untuk memproses permintaan jika semuanya valid. Kami juga telah menggunakan fungsi .param() dari Express.js untuk mengekstrak userId .

Pada fungsi .all() , kita meneruskan fungsi validateUserExists dari UsersMiddleware untuk dipanggil sebelum GET , PUT , PATCH , atau DELETE dapat melewati titik akhir /users/:userId . Ini berarti validateUserExists tidak perlu berada dalam larik fungsi tambahan yang kita teruskan ke .put() atau .patch() —itu akan dipanggil sebelum fungsi yang ditentukan di sana.

Kami telah memanfaatkan kemampuan penggunaan kembali yang melekat pada middleware di sini dengan cara lain juga. Dengan meneruskan UsersMiddleware.validateRequiredUserBodyFields untuk digunakan dalam konteks POST dan PUT , kami secara elegan menggabungkannya kembali dengan fungsi middleware lainnya.

Penafian: Kami hanya membahas validasi dasar dalam artikel ini. Dalam proyek dunia nyata, Anda perlu memikirkan dan menemukan semua batasan yang Anda perlukan untuk membuat kode. Demi kesederhanaan, kami juga mengasumsikan bahwa pengguna tidak dapat mengubah email mereka.

Menguji API REST Express/TypeScript Kami

Kami sekarang dapat mengkompilasi dan menjalankan aplikasi Node.js kami. Setelah dijalankan, kami siap untuk menguji rute API kami menggunakan klien REST seperti Postman atau cURL.

Pertama-tama mari kita coba untuk mendapatkan pengguna kami:

 curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'

Pada titik ini, kita akan memiliki array kosong sebagai respons, yang akurat. Sekarang kita dapat mencoba membuat sumber daya pengguna pertama dengan ini:

 curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'

Perhatikan bahwa sekarang aplikasi Node.js kami akan mengirimkan kembali kesalahan dari middleware kami:

 { "error": "Missing required fields email and password" }

Untuk memperbaikinya, mari kirimkan permintaan yang valid untuk memposting ke /users resource:

 curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'

Kali ini, kita akan melihat sesuatu seperti berikut:

 { "id": "ksVnfnPVW" }

id ini adalah pengidentifikasi pengguna yang baru dibuat dan akan berbeda di mesin Anda. Untuk membuat pernyataan pengujian yang tersisa lebih mudah, Anda dapat menjalankan perintah ini dengan yang Anda dapatkan (dengan asumsi Anda menggunakan lingkungan seperti Linux):

 REST_API_EXAMPLE_

Kita sekarang dapat melihat respons yang kita dapatkan dari membuat permintaan GET menggunakan variabel di atas:

 curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'

Kami sekarang juga dapat memperbarui seluruh sumber daya dengan permintaan PUT berikut:

 curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }'

Kami juga dapat menguji apakah validasi kami berfungsi dengan mengubah alamat email, yang akan menghasilkan kesalahan.

Perhatikan bahwa saat menggunakan PUT ke ID sumber daya, kami, sebagai konsumen API, perlu mengirim seluruh objek jika kami ingin menyesuaikan dengan pola REST standar. Artinya, jika kita hanya ingin mengupdate field lastName saja, tetapi menggunakan endpoint PUT kita, kita akan dipaksa untuk mengirimkan seluruh objek yang akan diupdate. Akan lebih mudah untuk menggunakan permintaan PATCH karena masih dalam batasan REST standar untuk mengirim hanya bidang lastName :

 curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }'

Ingatlah bahwa dalam basis kode kami sendiri, konfigurasi rute kami yang memberlakukan perbedaan antara PUT dan PATCH ini menggunakan fungsi middleware yang kami tambahkan di artikel ini.

PUT , PATCH , atau Keduanya?

Kedengarannya seperti tidak banyak alasan untuk mendukung PUT mengingat fleksibilitas PATCH , dan beberapa API akan mengambil pendekatan itu. Orang lain mungkin bersikeras untuk mendukung PUT untuk membuat API "sepenuhnya sesuai dengan REST", dalam hal ini, membuat rute PUT per bidang mungkin merupakan taktik yang sesuai untuk kasus penggunaan umum.

Pada kenyataannya, poin-poin ini adalah bagian dari diskusi yang jauh lebih besar mulai dari perbedaan kehidupan nyata antara keduanya hingga semantik yang lebih fleksibel untuk PATCH saja. Kami menyajikan dukungan PUT di sini dan semantik PATCH yang digunakan secara luas untuk kesederhanaan, tetapi mendorong pembaca untuk menyelidiki lebih lanjut penelitian ketika mereka merasa siap untuk melakukannya.

Mendapatkan daftar pengguna lagi seperti yang kita lakukan di atas, kita akan melihat pengguna yang kita buat dengan bidangnya diperbarui:

 [ { "id": "ksVnfnPVW", "email": "[email protected]", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ]

Akhirnya, kami dapat menguji penghapusan pengguna dengan ini:

 curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'

Mendapatkan daftar pengguna lagi, kita akan melihat bahwa pengguna yang dihapus tidak lagi ada.

Dengan itu, kami memiliki semua operasi CRUD untuk sumber daya users yang berfungsi.

Node.js/TypeScript REST API

Di bagian seri ini, kami mengeksplorasi lebih lanjut langkah-langkah kunci dalam membangun REST API menggunakan Express.js. Kami membagi kode kami untuk mendukung layanan, middleware, pengontrol, dan model. Masing-masing fungsinya memiliki peran tertentu, baik itu validasi, operasi logis, atau pemrosesan permintaan yang valid dan meresponsnya.

Kami juga menciptakan cara yang sangat sederhana untuk menyimpan data, dengan tujuan (maafkan permainan kata-kata) yang memungkinkan beberapa pengujian pada saat ini, kemudian diganti dengan sesuatu yang lebih praktis di bagian selanjutnya dari seri kami.

Selain membangun API dengan mempertimbangkan kesederhanaan—menggunakan kelas tunggal, misalnya—ada beberapa langkah yang harus diambil untuk membuatnya lebih mudah dirawat, lebih skalabel, dan aman. Dalam artikel terakhir dalam seri ini, kami membahas:

  • Mengganti database dalam memori dengan MongoDB, kemudian menggunakan Mongoose untuk menyederhanakan proses pengkodean
  • Menambahkan lapisan keamanan dan mengontrol akses dalam pendekatan stateless dengan JWT
  • Mengonfigurasi pengujian otomatis agar aplikasi kita dapat diskalakan

Anda dapat menelusuri kode terakhir dari artikel ini di sini.