Membangun API REST Node.js/TypeScript, Bagian 1: Express.js
Diterbitkan: 2022-03-11Bagaimana Saya Menulis REST API di Node.js?
Saat membangun back end untuk REST API, Express.js sering kali menjadi pilihan pertama di antara framework Node.js. Meskipun juga mendukung pembuatan HTML dan template statis, dalam seri ini, kami akan fokus pada pengembangan back-end menggunakan TypeScript. REST API yang dihasilkan akan menjadi salah satu kerangka kerja front-end atau layanan back-end eksternal yang dapat melakukan kueri.
Kamu akan membutuhkan:
- Pengetahuan dasar tentang JavaScript dan TypeScript
- Pengetahuan dasar tentang Node.js
- Pengetahuan dasar tentang arsitektur REST (lih. bagian ini dari artikel REST API saya sebelumnya jika diperlukan)
- Instalasi Node.js yang sudah siap (sebaiknya versi 14+)
Di terminal (atau command prompt), kami akan membuat folder untuk proyek tersebut. Dari folder itu, jalankan npm init
. Itu akan membuat beberapa file proyek Node.js dasar yang kita butuhkan.
Selanjutnya, kita akan menambahkan framework Express.js dan beberapa library yang berguna:
npm i express debug winston express-winston cors
Ada alasan bagus mengapa perpustakaan ini adalah favorit pengembang Node.js:
-
debug
adalah modul yang akan kita gunakan untuk menghindari pemanggilanconsole.log()
saat mengembangkan aplikasi kita. Dengan cara ini, kita dapat dengan mudah memfilter pernyataan debug selama pemecahan masalah. Mereka juga dapat dimatikan sepenuhnya dalam produksi daripada harus dihapus secara manual. -
winston
bertanggung jawab untuk mencatat permintaan ke API kami dan tanggapan (dan kesalahan) dikembalikan.express-winston
terintegrasi langsung dengan Express.js, sehingga semua kode loggingwinston
terkait API standar sudah selesai. -
cors
adalah bagian dari middleware Express.js yang memungkinkan kita mengaktifkan berbagi sumber daya lintas-Asal. Tanpa ini, API kami hanya dapat digunakan dari ujung depan yang dilayani dari subdomain yang sama persis dengan ujung belakang kami.
Bagian belakang kami menggunakan paket-paket ini saat sedang berjalan. Tetapi kita juga perlu menginstal beberapa dependensi pengembangan untuk konfigurasi TypeScript kita. Untuk itu, kami akan menjalankan:
npm i --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript
Dependensi ini diperlukan untuk mengaktifkan TypeScript untuk kode aplikasi kita sendiri, bersama dengan tipe yang digunakan oleh Express.js dan dependensi lainnya. Ini dapat menghemat banyak waktu ketika kita menggunakan IDE seperti WebStorm atau VSCode dengan memungkinkan kita untuk menyelesaikan beberapa metode fungsi secara otomatis saat coding.
Dependensi terakhir dalam package.json
harus seperti ini:
"dependencies": { "debug": "^4.2.0", "express": "^4.17.1", "express-winston": "^4.0.5", "winston": "^3.3.3", "cors": "^2.8.5" }, "devDependencies": { "@types/cors": "^2.8.7", "@types/debug": "^4.1.5", "@types/express": "^4.17.2", "source-map-support": "^0.5.16", "tslint": "^6.0.0", "typescript": "^3.7.5" }
Sekarang kita telah menginstal semua dependensi yang diperlukan, mari kita mulai membangun kode kita sendiri!
Struktur Proyek TypeScript REST API
Untuk tutorial ini, kita hanya akan membuat tiga file:
-
./app.ts
-
./common/common.routes.config.ts
-
./users/users.routes.config.ts
Gagasan di balik dua folder struktur proyek ( common
dan users
) adalah memiliki modul individual yang memiliki tanggung jawab mereka sendiri. Dalam pengertian ini, kita pada akhirnya akan memiliki beberapa atau semua hal berikut untuk setiap modul:
- Konfigurasi rute untuk menentukan permintaan yang dapat ditangani oleh API kami
- Layanan untuk tugas-tugas seperti menghubungkan ke model database kami, melakukan kueri, atau menghubungkan ke layanan eksternal yang diperlukan oleh permintaan tertentu
- Middleware untuk menjalankan validasi permintaan khusus sebelum pengontrol akhir dari suatu rute menangani spesifikasinya
- Model untuk mendefinisikan model data yang cocok dengan skema database yang diberikan, untuk memfasilitasi penyimpanan dan pengambilan data
- Pengontrol untuk memisahkan konfigurasi rute dari kode yang akhirnya (setelah middleware apa pun) memproses permintaan rute, memanggil fungsi layanan di atas jika perlu, dan memberikan respons kepada klien
Struktur folder ini menyediakan desain REST API dasar, titik awal awal untuk sisa seri tutorial ini, dan cukup untuk mulai berlatih.
File Rute Umum di TypeScript
Di folder common
, mari kita buat file common.routes.config.ts
agar terlihat seperti berikut:
import express from 'express'; export class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; } getName() { return this.name; } }
Cara kita membuat rute di sini adalah opsional. Tetapi karena kami bekerja dengan TypeScript, skenario rute kami adalah kesempatan untuk berlatih menggunakan pewarisan dengan kata kunci extends
, seperti yang akan segera kita lihat. Dalam proyek ini, semua file rute memiliki perilaku yang sama: Mereka memiliki nama (yang akan kita gunakan untuk tujuan debugging) dan akses ke objek utama Application
Express.js.
Sekarang, kita dapat mulai membuat file rute pengguna. Pada folder users
, mari buat users.routes.config.ts
dan mulai mengkodekannya seperti ini:
import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } }
Di sini, kami mengimpor kelas CommonRoutesConfig
dan memperluasnya ke kelas baru kami, yang disebut UsersRoutes
. Dengan konstruktor, kami mengirim aplikasi (objek express.Application
utama) dan nama UsersRoutes ke konstruktor CommonRoutesConfig
.
Contoh ini cukup sederhana, tetapi ketika menskalakan untuk membuat beberapa file rute, ini akan membantu kita menghindari kode duplikat.
Misalkan kita ingin menambahkan fitur baru pada file ini, seperti logging. Kita bisa menambahkan bidang yang diperlukan ke kelas CommonRoutesConfig
, dan kemudian semua rute yang memperluas CommonRoutesConfig
akan memiliki akses ke sana.
Menggunakan Fungsi Abstrak TypeScript untuk Fungsi Serupa di Seluruh Kelas
Bagaimana jika kita ingin memiliki beberapa fungsionalitas yang serupa di antara kelas-kelas ini (seperti mengonfigurasi titik akhir API), tetapi itu memerlukan implementasi yang berbeda untuk setiap kelas? Salah satu opsi adalah menggunakan fitur TypeScript yang disebut abstraction .
Mari kita buat fungsi abstrak yang sangat sederhana yang akan diwarisi oleh kelas UsersRoutes
(dan kelas perutean mendatang) dari CommonRoutesConfig
. Katakanlah kita ingin memaksa semua rute memiliki fungsi (sehingga kita dapat memanggilnya dari konstruktor umum kita) bernama configureRoutes()
. Di situlah kami akan mendeklarasikan titik akhir dari setiap sumber daya kelas perutean.
Untuk melakukan ini, kami akan menambahkan tiga hal cepat ke common.routes.config.ts
:
- Kata kunci
abstract
ke barisclass
kami, untuk mengaktifkan abstraksi untuk kelas ini. - Deklarasi fungsi baru di akhir kelas kita,
abstract configureRoutes(): express.Application;
. Ini memaksa semua kelas yang memperluasCommonRoutesConfig
untuk menyediakan implementasi yang cocok dengan tanda tangan itu—jika tidak, kompiler TypeScript akan memunculkan kesalahan. - Panggilan ke
this.configureRoutes();
di akhir konstruktor, karena sekarang kita dapat yakin bahwa fungsi ini akan ada.
Hasil:
import express from 'express'; export abstract class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; this.configureRoutes(); } getName() { return this.name; } abstract configureRoutes(): express.Application; }
Dengan itu, setiap kelas yang memperluas CommonRoutesConfig
harus memiliki fungsi yang disebut configureRoutes()
yang mengembalikan objek express.Application
. Itu berarti users.routes.config.ts
perlu diperbarui:
import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes() { // (we'll add the actual route configuration here next) return this.app; } }
Sebagai rekap dari apa yang telah kami buat:
Pertama-tama kita mengimpor file common.routes.config
, kemudian modul express
. Kami kemudian mendefinisikan kelas UserRoutes
, dengan mengatakan bahwa kami ingin itu memperluas kelas dasar CommonRoutesConfig
, yang menyiratkan bahwa kami berjanji bahwa itu akan mengimplementasikan configureRoutes()
.
Untuk mengirim informasi bersama ke kelas CommonRoutesConfig
, kami menggunakan constructor
kelas. Ia mengharapkan untuk menerima objek express.Application
, yang akan kami jelaskan secara lebih mendalam di langkah berikutnya. Dengan super()
, kami meneruskan ke konstruktor CommonRoutesConfig
aplikasi dan nama rute kami, yang dalam skenario ini adalah UsersRoutes. ( super()
, pada gilirannya, akan memanggil implementasi kita dari configureRoutes()
.)
Mengonfigurasi Rute Express.js dari Titik Akhir Pengguna
Fungsi configureRoutes()
adalah tempat kita akan membuat titik akhir untuk pengguna REST API kita. Di sana, kita akan menggunakan aplikasi dan fungsionalitas rutenya dari Express.js.
Ide dalam menggunakan fungsi app.route()
adalah untuk menghindari duplikasi kode, yang mudah karena kita membuat REST API dengan sumber daya yang terdefinisi dengan baik. Sumber daya utama untuk tutorial ini adalah pengguna . Kami memiliki dua kasus dalam skenario ini:
- Saat pemanggil API ingin membuat pengguna baru atau mencantumkan semua pengguna yang ada, titik akhir awalnya hanya memiliki
users
di akhir jalur yang diminta. (Kami tidak akan membahas pemfilteran kueri, pagination, atau kueri sejenis lainnya di artikel ini.) - Saat pemanggil ingin melakukan sesuatu yang spesifik untuk catatan pengguna tertentu, jalur sumber daya permintaan akan mengikuti pola
users/:userId
.
Cara .route()
di Express.js memungkinkan kita menangani verba HTTP dengan beberapa rangkaian yang elegan. Ini karena .get()
, .post()
, dll., semuanya mengembalikan instance IRoute
yang sama dengan panggilan .route()
pertama. Konfigurasi akhir akan seperti ini:
configureRoutes() { this.app.route(`/users`) .get((req: express.Request, res: express.Response) => { res.status(200).send(`List of users`); }) .post((req: express.Request, res: express.Response) => { res.status(200).send(`Post to users`); }); this.app.route(`/users/:userId`) .all((req: express.Request, res: express.Response, next: express.NextFunction) => { // this middleware function runs before any request to /users/:userId // but it doesn't accomplish anything just yet--- // it simply passes control to the next applicable function below using next() next(); }) .get((req: express.Request, res: express.Response) => { res.status(200).send(`GET requested for id ${req.params.userId}`); }) .put((req: express.Request, res: express.Response) => { res.status(200).send(`PUT requested for id ${req.params.userId}`); }) .patch((req: express.Request, res: express.Response) => { res.status(200).send(`PATCH requested for id ${req.params.userId}`); }) .delete((req: express.Request, res: express.Response) => { res.status(200).send(`DELETE requested for id ${req.params.userId}`); }); return this.app; }
Kode di atas memungkinkan setiap klien REST API memanggil titik akhir users
kami dengan permintaan POST
atau GET
. Demikian pula, ini memungkinkan klien memanggil titik akhir /users/:userId
kami dengan permintaan GET
, PUT
, PATCH
, atau DELETE
.

Tetapi untuk /users/:userId
, kami juga telah menambahkan middleware generik menggunakan fungsi all()
, yang akan dijalankan sebelum fungsi get()
, put()
, patch()
, atau delete()
mana pun. Fungsi ini akan bermanfaat ketika (selanjutnya dalam seri) kita membuat rute yang dimaksudkan untuk diakses hanya oleh pengguna yang diautentikasi.
Anda mungkin telah memperhatikan bahwa dalam fungsi .all()
kami—seperti halnya middleware mana pun—kami memiliki tiga jenis bidang: Request
, Response
, dan NextFunction
.
- Permintaan adalah cara Express.js mewakili permintaan HTTP untuk ditangani. Jenis ini meningkatkan dan memperluas jenis permintaan Node.js asli.
- Responnya juga sama dengan cara Express.js merepresentasikan respons HTTP, sekali lagi memperluas tipe respons Node.js asli.
- Tidak kalah pentingnya,
NextFunction
berfungsi sebagai fungsi callback, memungkinkan kontrol untuk melewati fungsi middleware lainnya. Sepanjang jalan, semua middleware akan berbagi permintaan dan objek respons yang sama sebelum pengontrol akhirnya mengirimkan respons kembali ke pemohon.
File Titik Masuk Node.js kami, app.ts
Sekarang kita telah mengonfigurasi beberapa kerangka rute dasar, kita akan mulai mengonfigurasi titik masuk aplikasi. Mari buat file app.ts
di root folder proyek kita dan mulai dengan kode ini:
import express from 'express'; import * as http from 'http'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; import cors from 'cors'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import debug from 'debug';
Hanya dua dari impor ini yang baru pada saat ini dalam artikel:
-
http
adalah modul asli Node.js. Diperlukan untuk memulai aplikasi Express.js kami. -
body-parser
adalah middleware yang disertakan dengan Express.js. Ini mem-parsing permintaan (dalam kasus kami, sebagai JSON) sebelum kontrol masuk ke penangan permintaan kami sendiri.
Sekarang setelah kita mengimpor file, kita akan mulai mendeklarasikan variabel yang ingin kita gunakan:
const app: express.Application = express(); const server: http.Server = http.createServer(app); const port = 3000; const routes: Array<CommonRoutesConfig> = []; const debugLog: debug.IDebugger = debug('app');
Fungsi express()
mengembalikan objek aplikasi Express.js utama yang akan kita bagikan di seluruh kode kita, dimulai dengan menambahkannya ke objek http.Server
. (Kita perlu memulai http.Server
setelah mengonfigurasi express.Application
.)
Kita akan mendengarkan pada port 3000—yang secara otomatis akan disimpulkan oleh TypeScript adalah sebuah Number
—bukan port standar 80 (HTTP) atau 443 (HTTPS) karena port tersebut biasanya digunakan untuk front end aplikasi.
Mengapa Port 3000?
Tidak ada aturan bahwa port harus 3000—jika tidak ditentukan, port arbitrer akan ditetapkan—tetapi 3000 digunakan di seluruh contoh dokumentasi untuk Node.js dan Express.js, jadi kami melanjutkan tradisi di sini.
Bisakah Node.js Berbagi Port Dengan Front End?
Kami masih dapat berjalan secara lokal di port kustom, bahkan ketika kami ingin backend kami menanggapi permintaan pada port standar. Ini akan membutuhkan proxy terbalik untuk menerima permintaan pada port 80 atau 443 dengan domain atau subdomain tertentu. Itu kemudian akan mengarahkan mereka ke port internal kami 3000.
Array routes
akan melacak file rute kami untuk tujuan debugging, seperti yang akan kita lihat di bawah.
Terakhir, debugLog
akan berakhir sebagai fungsi yang mirip dengan console.log
, tetapi lebih baik: Lebih mudah untuk menyempurnakannya karena secara otomatis dicakupkan ke apa pun yang kita inginkan untuk memanggil konteks file/modul kita. (Dalam hal ini, kami menyebutnya "aplikasi" ketika kami meneruskannya dalam string ke konstruktor debug()
.)
Sekarang, kami siap untuk mengonfigurasi semua modul middleware Express.js dan rute API kami:
// here we are adding middleware to parse all incoming requests as JSON app.use(express.json()); // here we are adding middleware to allow cross-origin requests app.use(cors()); // here we are preparing the expressWinston logging middleware configuration, // which will automatically log all HTTP requests handled by Express.js const loggerOptions: expressWinston.LoggerOptions = { transports: [new winston.transports.Console()], format: winston.format.combine( winston.format.json(), winston.format.prettyPrint(), winston.format.colorize({ all: true }) ), }; if (!process.env.DEBUG) { loggerOptions.meta = false; // when not debugging, log requests as one-liners } // initialize the logger with the above configuration app.use(expressWinston.logger(loggerOptions)); // here we are adding the UserRoutes to our array, // after sending the Express.js application object to have the routes added to our app! routes.push(new UsersRoutes(app)); // this is a simple route to make sure everything is working properly const runningMessage = `Server running at http://localhost:${port}`; app.get('/', (req: express.Request, res: express.Response) => { res.status(200).send(runningMessage) });
expressWinston.logger
terhubung ke Express.js, secara otomatis mencatat detail—melalui infrastruktur yang sama dengan debug
—untuk setiap permintaan yang diselesaikan. Opsi yang telah kita berikan padanya akan dengan rapi memformat dan mewarnai output terminal yang sesuai, dengan lebih banyak pencatatan verbose (default) saat kita berada dalam mode debug.
Perhatikan bahwa kita harus menentukan rute setelah kita mengatur expressWinston.logger
.
Terakhir dan yang terpenting:
server.listen(port, () => { routes.forEach((route: CommonRoutesConfig) => { debugLog(`Routes configured for ${route.getName()}`); }); // our only exception to avoiding console.log(), because we // always want to know when the server is done starting up console.log(runningMessage); });
Ini sebenarnya memulai server kami. Setelah dimulai, Node.js akan menjalankan fungsi panggilan balik kami, yang dalam mode debug melaporkan nama semua rute yang telah kami konfigurasikan—sejauh ini, hanya UsersRoutes
. Setelah itu, panggilan balik kami memberi tahu kami bahwa bagian belakang kami siap menerima permintaan, bahkan saat berjalan dalam mode produksi.
Memperbarui package.json
ke Transpile TypeScript ke JavaScript dan Jalankan Aplikasi
Sekarang setelah kerangka kita siap untuk dijalankan, pertama-tama kita memerlukan beberapa konfigurasi boilerplate untuk mengaktifkan transpilasi TypeScript. Mari tambahkan file tsconfig.json
di root proyek:
{ "compilerOptions": { "target": "es2016", "module": "commonjs", "outDir": "./dist", "strict": true, "esModuleInterop": true, "inlineSourceMap": true } }
Kemudian kita tinggal menambahkan sentuhan akhir pada package.json
berupa script berikut:
"scripts": { "start": "tsc && node --unhandled-rejections=strict ./dist/app.js", "debug": "export DEBUG=* && npm run start", "test": "echo \"Error: no test specified\" && exit 1" },
Skrip test
adalah pengganti yang akan kami ganti nanti di seri.
Tsc dalam skrip start
milik TypeScript. Ini bertanggung jawab untuk mentranspilasikan kode TypeScript kami ke dalam JavaScript, yang akan ditampilkan ke folder dist
. Kemudian, kami hanya menjalankan versi yang dibangun dengan node ./dist/app.js
.
Kami meneruskan --unhandled-rejections=strict
ke Node.js (bahkan dengan Node.js v16+) karena dalam praktiknya, debugging menggunakan pendekatan langsung “crash and show the stack” lebih mudah daripada logging yang lebih bagus dengan objek expressWinston.errorLogger
. Ini paling sering benar bahkan dalam produksi, di mana membiarkan Node.js terus berjalan meskipun penolakan yang tidak tertangani kemungkinan akan meninggalkan server dalam keadaan tak terduga, memungkinkan bug lebih lanjut (dan lebih rumit) terjadi.
Skrip debug
memanggil skrip start
tetapi pertama-tama mendefinisikan variabel lingkungan DEBUG
. Ini memiliki efek mengaktifkan semua pernyataan debugLog()
kami (ditambah yang serupa dari Express.js itu sendiri, yang menggunakan modul debug
yang sama seperti yang kami lakukan) untuk menampilkan detail yang berguna ke terminal—detail yang (dengan mudah) jika tidak disembunyikan saat dijalankan server dalam mode produksi dengan npm start
standar.
Coba jalankan npm run debug
sendiri, dan setelah itu, bandingkan dengan npm start
untuk melihat bagaimana output konsol berubah.
Tip: Anda dapat membatasi keluaran debug ke pernyataan debugLog()
file app.ts
kami sendiri menggunakan DEBUG=app
alih-alih DEBUG=*
. Modul debug
umumnya cukup fleksibel, dan fitur ini tidak terkecuali.
Pengguna Windows mungkin perlu mengubah export
ke SET
karena export
adalah cara kerjanya di Mac dan Linux. Jika proyek Anda perlu mendukung beberapa lingkungan pengembangan, paket lintas-env menyediakan solusi langsung di sini.
Menguji Back End Live Express.js
Dengan npm run debug
atau npm start
masih berjalan, REST API kami akan siap melayani permintaan pada port 3000. Pada titik ini, kami dapat menggunakan cURL, Postman, Insomnia, dll. untuk menguji back end.
Karena kami hanya membuat kerangka untuk sumber daya pengguna, kami cukup mengirim permintaan tanpa badan untuk melihat bahwa semuanya berfungsi seperti yang diharapkan. Sebagai contoh:
curl --request GET 'localhost:3000/users/12345'
Bagian belakang kami harus mengirim kembali jawaban GET requested for id 12345
.
Adapun POST
:
curl --request POST 'localhost:3000/users' \ --data-raw ''
Ini dan semua jenis permintaan lain yang kami buat untuk kerangka akan terlihat sangat mirip.
Siap untuk Pengembangan REST API Rapid Node.js dengan TypeScript
Pada artikel ini, kami mulai membuat REST API dengan mengonfigurasi proyek dari awal dan mempelajari dasar-dasar framework Express.js. Kemudian, kami mengambil langkah pertama untuk menguasai TypeScript dengan membangun sebuah pola dengan UsersRoutesConfig
memperluas CommonRoutesConfig
, sebuah pola yang akan kami gunakan kembali untuk artikel berikutnya dalam seri ini. Kami selesai dengan mengonfigurasi titik masuk app.ts
kami untuk menggunakan rute baru dan package.json
kami dengan skrip untuk membangun dan menjalankan aplikasi kami.
Tetapi bahkan dasar-dasar REST API yang dibuat dengan Express.js dan TypeScript cukup terlibat. Di bagian selanjutnya dari seri ini, kami fokus pada pembuatan pengontrol yang tepat untuk sumber daya pengguna dan menggali beberapa pola yang berguna untuk layanan, middleware, pengontrol, dan model.
Proyek lengkapnya tersedia di GitHub, dan kode pada akhir artikel ini dapat ditemukan di cabang toptal-article-01
.