Memulai Dengan Express Dan ES6+ JavaScript Stack

Diterbitkan: 2022-03-10
Ringkasan cepat Pengenalan dasar untuk proses pengembangan aplikasi web backend dengan Express — membahas fitur JavaScript ES6+ yang terbaru, Pola Desain Pabrik, Operasi CRUD MongoDB, server dan port, dan masa depan dengan pola arsitektur n-tier perusahaan untuk proyek TypeScript.

Artikel ini adalah bagian kedua dalam seri, dengan bagian satu terletak di sini, yang memberikan wawasan dasar dan (semoga) intuitif tentang Node.js, ES6+ JavaScript, Fungsi Panggilan Balik, Fungsi Panah, API, Protokol HTTP, JSON, MongoDB, dan lagi.

Dalam artikel ini, kita akan membangun keterampilan yang kita peroleh di artikel sebelumnya, mempelajari cara menerapkan dan menerapkan Basis Data MongoDB untuk menyimpan informasi daftar buku pengguna, membangun API dengan Node.js dan kerangka kerja Aplikasi Web Ekspres untuk mengekspos basis data tersebut dan melakukan Operasi CRUD di atasnya, dan banyak lagi. Sepanjang jalan, kita akan membahas ES6 Object Destructuring, ES6 Object Shorthand, sintaks Async/Await, Spread Operator, dan kita akan melihat sekilas tentang CORS, Same Origin Policy, dan banyak lagi.

Dalam artikel selanjutnya, kami akan memfaktorkan ulang basis kode kami untuk memisahkan masalah dengan memanfaatkan arsitektur tiga lapis dan mencapai Inversi Kontrol melalui Injeksi Ketergantungan, kami akan melakukan JSON Web Token dan Firebase Authentication berbasis keamanan dan kontrol akses, pelajari cara mengamankan menyimpan kata sandi, dan menggunakan AWS Simple Storage Service untuk menyimpan avatar pengguna dengan Buffer dan Stream Node.js — sambil memanfaatkan PostgreSQL untuk persistensi data. Sepanjang jalan, kami akan menulis ulang basis kode kami dari bawah ke atas di TypeScript untuk memeriksa konsep OOP Klasik (seperti Polimorfisme, Warisan, Komposisi, dan sebagainya) dan bahkan pola desain seperti Pabrik dan Adaptor.

Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Sebuah Kata Peringatan

Ada masalah dengan sebagian besar artikel yang membahas Node.js di luar sana hari ini. Sebagian besar dari mereka, tidak semuanya, tidak lebih dari menggambarkan cara mengatur Perutean Ekspres, mengintegrasikan Mongoose, dan mungkin menggunakan Otentikasi Token Web JSON. Masalahnya adalah mereka tidak berbicara tentang arsitektur, atau praktik terbaik keamanan, atau tentang prinsip pengkodean yang bersih, atau Kepatuhan ACID, Basis Data Relasional, Bentuk Normal Kelima, Teorema CAP, atau Transaksi. Baik diasumsikan bahwa Anda tahu tentang semua yang masuk, atau bahwa Anda tidak akan membangun proyek besar atau cukup populer untuk menjamin pengetahuan yang disebutkan di atas.

Tampaknya ada beberapa jenis pengembang Node yang berbeda — antara lain, beberapa baru dalam pemrograman secara umum, dan yang lain berasal dari sejarah panjang pengembangan perusahaan dengan C# dan .NET Framework atau Java Spring Framework. Sebagian besar artikel melayani kelompok sebelumnya.

Dalam artikel ini, saya akan melakukan persis seperti yang baru saja saya nyatakan bahwa terlalu banyak artikel dilakukan, tetapi dalam artikel lanjutan, kami akan memperbaiki basis kode kami sepenuhnya, memungkinkan saya untuk menjelaskan prinsip-prinsip seperti Injeksi Ketergantungan, Tiga- Arsitektur Lapisan (Pengontrol/Layanan/Repositori), Pemetaan Data dan Rekaman Aktif, pola desain, unit, integrasi, dan pengujian mutasi, Prinsip SOLID, Unit Kerja, pengkodean terhadap antarmuka, praktik terbaik keamanan seperti HSTS, CSRF, NoSQL, dan Injeksi SQL Pencegahan, dan sebagainya. Kami juga akan bermigrasi dari MongoDB ke PostgreSQL, menggunakan pembuat kueri sederhana Knex alih-alih ORM — memungkinkan kami untuk membangun infrastruktur akses data kami sendiri dan untuk lebih dekat dan pribadi dengan Bahasa Kueri Terstruktur, berbagai jenis hubungan (Satu- ke-Satu, Banyak-ke-Banyak, dll.), dan banyak lagi. Artikel ini, kemudian, harus menarik bagi pemula, tetapi beberapa artikel berikutnya harus melayani lebih banyak pengembang menengah yang ingin meningkatkan arsitektur mereka.

Dalam hal ini, kita hanya akan khawatir tentang data buku yang bertahan. Kami tidak akan menangani otentikasi pengguna, hashing kata sandi, arsitektur, atau sesuatu yang rumit seperti itu. Semua itu akan datang di artikel berikutnya dan yang akan datang. Untuk saat ini, dan pada dasarnya, kami hanya akan membangun sebuah metode yang memungkinkan klien untuk berkomunikasi dengan server web kami melalui Protokol HTTP untuk menyimpan informasi buku dalam database.

Catatan : Saya sengaja membuatnya sangat sederhana dan mungkin tidak terlalu praktis di sini karena artikel ini, dengan sendirinya, sangat panjang, karena saya telah mengambil kebebasan menyimpang untuk membahas topik tambahan. Dengan demikian, kami akan secara bertahap meningkatkan kualitas dan kompleksitas API pada seri ini, tetapi sekali lagi, karena saya menganggap ini sebagai salah satu perkenalan pertama Anda dengan Express, saya sengaja membuat semuanya sangat sederhana.

  1. Penghancuran Objek ES6
  2. Singkatan Objek ES6
  3. ES6 Spread Operator (...)
  4. Akan datang...

Penghancuran Objek ES6

ES6 Object Destructuring, atau Destructuring Assignment Syntax, adalah metode yang digunakan untuk mengekstrak atau membongkar nilai dari array atau objek ke dalam variabelnya sendiri. Kita akan mulai dengan properti objek dan kemudian membahas elemen array.

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; // Log properties: console.log('Name:', person.name); console.log('Occupation:', person.occupation);

Operasi semacam itu cukup primitif, tetapi bisa sedikit merepotkan mengingat kita harus terus merujuk orang. person.something di mana-mana. Misalkan ada 10 tempat lain di seluruh kode kita di mana kita harus melakukan itu — itu akan menjadi cukup sulit dengan cepat. Metode singkatnya adalah menetapkan nilai-nilai ini ke variabel mereka sendiri.

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; const personName = person.name; const personOccupation = person.occupation; // Log properties: console.log('Name:', personName); console.log('Occupation:', personOccupation);

Mungkin ini terlihat masuk akal, tetapi bagaimana jika kita memiliki 10 properti lain yang bersarang di objek person juga? Itu akan menjadi banyak baris yang tidak perlu hanya untuk menetapkan nilai ke variabel — pada titik mana kita berada dalam bahaya karena jika properti objek bermutasi, variabel kita tidak akan mencerminkan perubahan itu (ingat, hanya referensi ke objek yang tidak dapat diubah dengan penetapan const , bukan properti objek), jadi pada dasarnya, kita tidak bisa lagi menjaga "status" (dan saya menggunakan kata itu secara longgar) sinkron. Pass by reference vs pass by value mungkin berperan di sini, tetapi saya tidak ingin menyimpang terlalu jauh dari cakupan bagian ini.

ES6 Object Destructing pada dasarnya memungkinkan kita melakukan ini:

 const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; // This is new. It's called Object Destructuring. const { name, occupation } = person; // Log properties: console.log('Name:', name); console.log('Occupation:', occupation);

Kami tidak membuat objek/objek literal baru, kami membongkar name dan properti occupation dari objek asli dan memasukkannya ke dalam variabel mereka sendiri dengan nama yang sama. Nama yang kita gunakan harus sesuai dengan nama properti yang ingin kita ekstrak.

Sekali lagi, sintaks const { a, b } = someObject; secara khusus mengatakan bahwa kami mengharapkan beberapa properti a dan beberapa properti b ada di dalam someObject (yaitu, someObject dapat berupa { a: 'dataA', b: 'dataB' } , misalnya) dan bahwa kami ingin menempatkan apa pun nilainya dari kunci/properti tersebut dalam variabel const dengan nama yang sama. Itu sebabnya sintaks di atas akan memberi kita dua variabel const a = someObject.a dan const b = someObject.b .

Artinya, ada dua sisi dari Object Destructuring. Sisi "Templat" dan sisi "Sumber", di mana sisi const { a, b } (sisi kiri) adalah template dan sisi someObject (sisi kanan) adalah sisi sumber — yang masuk akal — kami mendefinisikan struktur atau "templat" di sebelah kiri yang mencerminkan data di sisi "sumber".

Sekali lagi, untuk memperjelasnya, berikut adalah beberapa contoh:

 // ----- Destructure from Object Variable with const ----- // const objOne = { a: 'dataA', b: 'dataB' }; // Destructure const { a, b } = objOne; console.log(a); // dataA console.log(b); // dataB // ----- Destructure from Object Variable with let ----- // let objTwo = { c: 'dataC', d: 'dataD' }; // Destructure let { c, d } = objTwo; console.log(c); // dataC console.log(d); // dataD // Destructure from Object Literal with const ----- // const { e, f } = { e: 'dataE', f: 'dataF' }; // <-- Destructure console.log(e); // dataE console.log(f); // dataF // Destructure from Object Literal with let ----- // let { g, h } = { g: 'dataG', h: 'dataH' }; // <-- Destructure console.log(g); // dataG console.log(h); // dataH

Dalam kasus properti bersarang, cerminkan struktur yang sama dalam tugas penghancuran Anda:

 const person = { name: 'Richard P. Feynman', occupation: { type: 'Theoretical Physicist', location: { lat: 1, lng: 2 } } }; // Attempt one: const { name, occupation } = person; console.log(name); // Richard P. Feynman console.log(occupation); // The entire `occupation` object. // Attempt two: const { occupation: { type, location } } = person; console.log(type); // Theoretical Physicist console.log(location) // The entire `location` object. // Attempt three: const { occupation: { location: { lat, lng } } } = person; console.log(lat); // 1 console.log(lng); // 2

Seperti yang Anda lihat, properti yang Anda putuskan untuk ditarik adalah opsional, dan untuk membongkar properti bersarang, cukup cerminkan struktur objek asli (sumber) di sisi template sintaks perusak Anda. Jika Anda mencoba untuk merusak properti yang tidak ada pada objek asli, nilai itu tidak akan ditentukan.

Kami juga dapat merusak struktur variabel tanpa terlebih dahulu mendeklarasikannya — penugasan tanpa deklarasi — menggunakan sintaks berikut:

 let name, occupation; const person = { name: 'Richard P. Feynman', occupation: 'Theoretical Physicist' }; ;({ name, occupation } = person); console.log(name); // Richard P. Feynman console.log(occupation); // Theoretical Physicist

Kami mendahului ekspresi dengan titik koma untuk memastikan kami tidak secara tidak sengaja membuat IIFE (Ekspresi Fungsi Segera Dipanggil) dengan fungsi pada baris sebelumnya (jika ada fungsi seperti itu), dan tanda kurung di sekitar pernyataan penugasan diperlukan untuk hentikan JavaScript agar tidak memperlakukan sisi kiri (templat) Anda sebagai blok.

Kasus penggunaan destrukturisasi yang sangat umum ada dalam argumen fungsi:

 const config = { baseUrl: '<baseURL>', awsBucket: '<bucket>', secret: '<secret-key>' // <- Make this an env var. }; // Destructures `baseUrl` and `awsBucket` off `config`. const performOperation = ({ baseUrl, awsBucket }) => { fetch(baseUrl).then(() => console.log('Done')); console.log(awsBucket); // <bucket> }; performOperation(config);

Seperti yang Anda lihat, kita bisa saja menggunakan sintaks destructuring normal yang sekarang kita gunakan di dalam fungsi, seperti ini:

 const config = { baseUrl: '<baseURL>', awsBucket: '<bucket>', secret: '<secret-key>' // <- Make this an env var. }; const performOperation = someConfig => { const { baseUrl, awsBucket } = someConfig; fetch(baseUrl).then(() => console.log('Done')); console.log(awsBucket); // <bucket> }; performOperation(config);

Tetapi menempatkan sintaks tersebut di dalam tanda tangan fungsi melakukan perusakan secara otomatis dan menyelamatkan kita dari satu baris.

Kasus penggunaan dunia nyata ini ada di React Functional Components for props :

 import React from 'react'; // Destructure `titleText` and `secondaryText` from `props`. export default ({ titleText, secondaryText }) => ( <div> <h1>{titleText}</h1> <h3>{secondaryText}</h3> </div> );

Sebagai lawan:

 import React from 'react'; export default props => ( <div> <h1>{props.titleText}</h1> <h3>{props.secondaryText}</h3> </div> );

Dalam kedua kasus, kami juga dapat menetapkan nilai default ke properti:

 const personOne = { name: 'User One', password: 'BCrypt Hash' }; const personTwo = { password: 'BCrypt Hash' }; const createUser = ({ name = 'Anonymous', password }) => { if (!password) throw new Error('InvalidArgumentException'); console.log(name); console.log(password); return { id: Math.random().toString(36) // <--- Should follow RFC 4122 Spec in real app. .substring(2, 15) + Math.random() .toString(36).substring(2, 15), name: name, // <-- We'll discuss this next. password: password // <-- We'll discuss this next. }; } createUser(personOne); // User One, BCrypt Hash createUser(personTwo); // Anonymous, BCrypt Hash

Seperti yang Anda lihat, jika name tersebut tidak ada saat dirusak, kami memberikannya nilai default. Kita dapat melakukan ini dengan sintaks sebelumnya juga:

 const { a, b, c = 'Default' } = { a: 'dataA', b: 'dataB' }; console.log(a); // dataA console.log(b); // dataB console.log(c); // Default

Array juga dapat dirusak:

 const myArr = [4, 3]; // Destructuring happens here. const [valOne, valTwo] = myArr; console.log(valOne); // 4 console.log(valTwo); // 3 // ----- Destructuring without assignment: ----- // let a, b; // Destructuring happens here. ;([a, b] = [10, 2]); console.log(a + b); // 12

Alasan praktis untuk destrukturisasi array terjadi dengan React Hooks. (Dan ada banyak alasan lain, saya hanya menggunakan React sebagai contoh).

 import React, { useState } from "react"; export default () => { const [buttonText, setButtonText] = useState("Default"); return ( <button onClick={() => setButtonText("Toggled")}> {buttonText} </button> ); }

Perhatikan useState sedang dirusak dari ekspor, dan fungsi/nilai array sedang dirusak dari kait useState . Sekali lagi, jangan khawatir jika hal di atas tidak masuk akal — Anda harus memahami React — dan saya hanya menggunakannya sebagai contoh.

Meskipun ada lebih banyak untuk ES6 Object Destructuring, saya akan membahas satu topik lagi di sini: Destructuring Renaming, yang berguna untuk mencegah tabrakan ruang lingkup atau bayangan variabel, dll. Misalkan kita ingin merusak properti bernama name dari objek bernama person , tapi sudah ada variabel dengan nama name di ruang lingkup. Kami dapat mengganti nama dengan cepat dengan titik dua:

 // JS Destructuring Naming Collision Example: const name = 'Jamie Corkhill'; const person = { name: 'Alan Turing' }; // Rename `name` from `person` to `personName` after destructuring. const { name: personName } = person; console.log(name); // Jamie Corkhill <-- As expected. console.log(personName); // Alan Turing <-- Variable was renamed.

Akhirnya, kita juga dapat menetapkan nilai default dengan mengganti nama:

 const name = 'Jamie Corkhill'; const person = { location: 'New York City, United States' }; const { name: personName = 'Anonymous', location } = person; console.log(name); // Jamie Corkhill console.log(personName); // Anonymous console.log(location); // New York City, United States

Seperti yang Anda lihat, dalam kasus ini, name dari person ( person.name ) akan diubah namanya menjadi personName dan disetel ke nilai default Anonymous jika tidak ada.

Dan tentu saja, hal yang sama dapat dilakukan dalam tanda tangan fungsi:

 const personOne = { name: 'User One', password: 'BCrypt Hash' }; const personTwo = { password: 'BCrypt Hash' }; const createUser = ({ name: personName = 'Anonymous', password }) => { if (!password) throw new Error('InvalidArgumentException'); console.log(personName); console.log(password); return { id: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), name: personName, password: password // <-- We'll discuss this next. }; } createUser(personOne); // User One, BCrypt Hash createUser(personTwo); // Anonymous, BCrypt Hash

Singkatan Objek ES6

Misalkan Anda memiliki pabrik berikut: (kami akan membahas pabrik nanti)

 const createPersonFactory = (name, location, position) => ({ name: name, location: location, position: position });

Seseorang mungkin menggunakan pabrik ini untuk membuat objek person , sebagai berikut. Juga, perhatikan bahwa pabrik secara implisit mengembalikan objek, terbukti dengan tanda kurung di sekitar tanda kurung Fungsi Panah.

 const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person); // { ... }

Itu yang sudah kita ketahui dari ES5 Object Literal Syntax. Perhatikan, bagaimanapun, dalam fungsi pabrik, bahwa nilai setiap properti adalah nama yang sama dengan pengidentifikasi properti (kunci) itu sendiri. Yaitu — location: location atau name: name . Ternyata itu adalah kejadian yang cukup umum dengan pengembang JS.

Dengan sintaks singkatan dari ES6, kami dapat mencapai hasil yang sama dengan menulis ulang pabrik sebagai berikut:

 const createPersonFactory = (name, location, position) => ({ name, location, position }); const person = createPersonFactory('Jamie', 'Texas', 'Developer'); console.log(person);

Menghasilkan keluaran:

 { name: 'Jamie', location: 'Texas', position: 'Developer' }

Penting untuk disadari bahwa kita hanya dapat menggunakan singkatan ini ketika objek yang ingin kita buat sedang dibuat secara dinamis berdasarkan variabel, di mana nama variabelnya sama dengan nama properti yang kita inginkan variabelnya ditetapkan.

Sintaks yang sama ini berfungsi dengan nilai objek:

 const createPersonFactory = (name, location, position, extra) => ({ name, location, position, extra // <- right here. }); const extra = { interests: [ 'Mathematics', 'Quantum Mechanics', 'Spacecraft Launch Systems' ], favoriteLanguages: [ 'JavaScript', 'C#' ] }; const person = createPersonFactory('Jamie', 'Texas', 'Developer', extra); console.log(person);

Menghasilkan keluaran:

 { name: 'Jamie', location: 'Texas', position: 'Developer', extra: { interests: [ 'Mathematics', 'Quantum Mechanics', 'Spacecraft Launch Systems' ], favoriteLanguages: [ 'JavaScript', 'C#' ] } }

Sebagai contoh terakhir, ini juga berfungsi dengan literal objek:

 const id = '314159265358979'; const name = 'Archimedes of Syracuse'; const location = 'Syracuse'; const greatMathematician = { id, name, location };

Operator Penyebaran ES6 (…)

Spread Operator mengizinkan kita untuk melakukan berbagai hal, beberapa di antaranya akan kita bahas di sini.

Pertama, kita dapat menyebarkan properti dari satu objek ke objek lain:

 const myObjOne = { a: 'a', b: 'b' }; const myObjTwo = { ...myObjOne }:

Ini memiliki efek menempatkan semua properti pada myObjOne ke myObjTwo , sehingga myObjTwo sekarang { a: 'a', b: 'b' } . Kita dapat menggunakan metode ini untuk mengganti properti sebelumnya. Misalkan pengguna ingin memperbarui akun mereka:

 const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */ const user = { name: 'John Doe', email: '[email protected]', password: ' ', bio: 'Lorem ipsum' }; const updates = { password: ' ', bio: 'Ipsum lorem', email: '[email protected]' }; const updatedUser = { ...user, // <- original ...updates // <- updates }; console.log(updatedUser); /* { name: 'John Doe', email: '[email protected]', // Updated password: ' ', // Updated bio: 'Ipsum lorem' } */

Hal yang sama dapat dilakukan dengan array:

 const apollo13Astronauts = ['Jim', 'Jack', 'Fred']; const apollo11Astronauts = ['Neil', 'Buz', 'Michael']; const unionOfAstronauts = [...apollo13Astronauts, ...apollo11Astronauts]; console.log(unionOfAstronauts); // ['Jim', 'Jack', 'Fred', 'Neil', 'Buz, 'Michael'];

Perhatikan di sini bahwa kita membuat gabungan dari kedua set (array) dengan menyebarkan array ke dalam array baru.

Ada lebih banyak lagi Operator Istirahat/Penyebaran, tetapi di luar cakupan artikel ini. Ini dapat digunakan untuk mencapai beberapa argumen ke suatu fungsi, misalnya. Jika Anda ingin mempelajari lebih lanjut, lihat Dokumentasi MDN di sini.

ES6 Asinkron/Menunggu

Async/Await adalah sintaks untuk mengurangi rasa sakit dari rantai janji.

Kata kunci yang dicadangkan await memungkinkan Anda untuk "menunggu" penyelesaian janji, tetapi hanya dapat digunakan dalam fungsi yang ditandai dengan kata kunci async . Misalkan saya memiliki fungsi yang mengembalikan janji. Dalam fungsi async baru, saya bisa await hasil dari janji itu daripada menggunakan .then dan .catch .

 // Returns a promise. const myFunctionThatReturnsAPromise = () => { return new Promise((resolve, reject) => { setTimeout(() => resolve('Hello'), 3000); }); } const myAsyncFunction = async () => { const promiseResolutionResult = await myFunctionThatReturnsAPromise(); console.log(promiseResolutionResult); }; // Writes the log statement after three seconds. myAsyncFunction();

Ada beberapa hal yang perlu diperhatikan di sini. Saat kita menggunakan await dalam fungsi async , hanya nilai yang diselesaikan yang masuk ke variabel di sisi kiri. Jika fungsi menolak, itu adalah kesalahan yang harus kita tangkap, seperti yang akan kita lihat sebentar lagi. Selain itu, fungsi apa pun yang ditandai async akan, secara default, mengembalikan janji.

Misalkan saya perlu membuat dua panggilan API, satu dengan respons dari yang pertama. Menggunakan janji dan rantai janji, Anda dapat melakukannya dengan cara ini:

 const makeAPICall = route => new Promise((resolve, reject) => { console.log(route) resolve(route); }); const main = () => { makeAPICall('/whatever') .then(response => makeAPICall(response + ' second call')) .then(response => console.log(response + ' logged')) .catch(err => console.error(err)) }; main(); // Result: /* /whatever /whatever second call /whatever second call logged */

Apa yang terjadi di sini adalah pertama-tama kita memanggil makeAPICall meneruskannya /whatever , yang akan dicatat pertama kali. Janji diselesaikan dengan nilai itu. Kemudian kami memanggil makeAPICall lagi, meneruskannya /whatever second call , yang dicatat, dan sekali lagi, janji diselesaikan dengan nilai baru itu. Akhirnya, kami mengambil nilai baru itu /whatever second call yang baru saja diselesaikan oleh janji, dan mencatatnya sendiri di log terakhir, menambahkan logged di akhir. Jika ini tidak masuk akal, Anda harus melihat ke dalam rantai janji.

Menggunakan async / await , kita dapat melakukan refactor sebagai berikut:

 const main = async () => { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); };

Inilah yang akan terjadi. Seluruh fungsi akan berhenti dieksekusi pada pernyataan makeAPICall await setelah resolusi, nilai yang diselesaikan akan ditempatkan di resultOne . Ketika itu terjadi, fungsi akan pindah ke pernyataan await kedua, sekali lagi berhenti di sana selama penyelesaian janji. Ketika janji diselesaikan, hasil resolusi akan ditempatkan di resultTwo . Jika ide tentang eksekusi fungsi terdengar menghalangi, jangan takut, itu masih tidak sinkron, dan saya akan membahas alasannya sebentar lagi.

Ini hanya menggambarkan jalan "bahagia". Jika salah satu janji ditolak, kita dapat menangkapnya dengan try/catch, karena jika janji ditolak, kesalahan akan dilemparkan — yang akan menjadi kesalahan apa pun yang ditolak janji.

 const main = async () => { try { const resultOne = await makeAPICall('/whatever'); const resultTwo = await makeAPICall(resultOne + ' second call'); console.log(resultTwo + ' logged'); } catch (e) { console.log(e) } };

Seperti yang saya katakan sebelumnya, fungsi apa pun yang dideklarasikan async akan mengembalikan janji. Jadi, jika Anda ingin memanggil fungsi async dari fungsi lain, Anda dapat menggunakan janji normal, atau await jika Anda mendeklarasikan fungsi panggilan async . Namun, jika Anda ingin memanggil fungsi async dari kode tingkat atas dan menunggu hasilnya, maka Anda harus menggunakan .then dan .catch .

Sebagai contoh:

 const returnNumberOne = async () => 1; returnNumberOne().then(value => console.log(value)); // 1

Atau, Anda dapat menggunakan Ekspresi Fungsi Segera Dipanggil (IIFE):

 (async () => { const value = await returnNumberOne(); console.log(value); // 1 })();

Saat Anda menggunakan await dalam fungsi async , eksekusi fungsi akan berhenti pada pernyataan menunggu sampai janji diselesaikan. Namun, semua fungsi lainnya bebas untuk melanjutkan eksekusi, sehingga tidak ada sumber daya CPU tambahan yang dialokasikan atau utas yang pernah diblokir. Saya akan mengatakannya lagi — operasi dalam fungsi khusus itu pada waktu tertentu akan berhenti sampai janji itu diselesaikan, tetapi semua fungsi lainnya bebas untuk diaktifkan. Pertimbangkan Server Web HTTP — berdasarkan permintaan, semua fungsi bebas diaktifkan untuk semua pengguna secara bersamaan saat permintaan dibuat, hanya saja sintaks async/await akan memberikan ilusi bahwa suatu operasi sinkron dan memblokir untuk membuat menjanjikan lebih mudah untuk dikerjakan, tetapi sekali lagi, semuanya akan tetap bagus dan asinkron.

Ini tidak semua yang ada untuk async / await , tetapi itu akan membantu Anda untuk memahami prinsip-prinsip dasar.

Pabrik OOP Klasik

Kita sekarang akan meninggalkan dunia JavaScript dan memasuki dunia Java . Ada saatnya ketika proses pembuatan suatu objek (dalam hal ini, turunan dari kelas — sekali lagi, Java) cukup rumit atau ketika kita ingin objek yang berbeda diproduksi berdasarkan serangkaian parameter. Contohnya mungkin fungsi yang membuat objek kesalahan yang berbeda. Pabrik adalah pola desain umum dalam Pemrograman Berorientasi Objek dan pada dasarnya adalah fungsi yang menciptakan objek. Untuk menjelajahi ini, mari kita beralih dari JavaScript ke dunia Java. Ini akan masuk akal bagi pengembang yang berasal dari OOP Klasik (yaitu, bukan prototipe), latar belakang bahasa yang diketik secara statis. Jika Anda bukan salah satu pengembang tersebut, silakan lewati bagian ini. Ini adalah penyimpangan kecil, jadi jika mengikuti di sini mengganggu aliran JavaScript Anda, sekali lagi, lewati bagian ini.

Pola kreasi umum, Pola Pabrik memungkinkan kita membuat objek tanpa memaparkan logika bisnis yang diperlukan untuk melakukan kreasi tersebut.

Misalkan kita sedang menulis program yang memungkinkan kita untuk memvisualisasikan bentuk primitif dalam n-dimensi. Jika kita menyediakan kubus, misalnya, kita akan melihat kubus 2D (persegi), kubus 3D (kubus), dan kubus 4D (Tesseract, atau Hypercube). Berikut adalah bagaimana hal ini dapat dilakukan, secara sepele, dan membatasi bagian gambar yang sebenarnya, di Jawa.

 // Main.java // Defining an interface for the shape (can be used as a base type) interface IShape { void draw(); } // Implementing the interface for 2-dimensions: class TwoDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 2D."); } } // Implementing the interface for 3-dimensions: class ThreeDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 3D."); } } // Implementing the interface for 4-dimensions: class FourDimensions implements IShape { @Override public void draw() { System.out.println("Drawing a shape in 4D."); } } // Handles object creation class ShapeFactory { // Factory method (notice return type is the base interface) public IShape createShape(int dimensions) { switch(dimensions) { case 2: return new TwoDimensions(); case 3: return new ThreeDimensions(); case 4: return new FourDimensions(); default: throw new IllegalArgumentException("Invalid dimension."); } } } // Main class and entry point. public class Main { public static void main(String[] args) throws Exception { ShapeFactory shapeFactory = new ShapeFactory(); IShape fourDimensions = shapeFactory.createShape(4); fourDimensions.draw(); // Drawing a shape in 4D. } }

Seperti yang Anda lihat, kami mendefinisikan antarmuka yang menentukan metode untuk menggambar bentuk. Dengan memiliki kelas yang berbeda mengimplementasikan antarmuka, kami dapat menjamin bahwa semua bentuk dapat digambar (karena mereka semua harus memiliki metode draw yang dapat diganti sesuai definisi antarmuka). Mengingat bentuk ini digambar secara berbeda tergantung pada dimensi yang dilihat, kami mendefinisikan kelas pembantu yang mengimplementasikan antarmuka untuk melakukan pekerjaan intensif GPU dalam mensimulasikan rendering n-dimensi. ShapeFactory melakukan pekerjaan membuat instance kelas yang benar — metode createShape adalah pabrik, dan seperti definisi di atas, ini adalah metode yang mengembalikan objek kelas. Tipe createShape adalah antarmuka IShape karena antarmuka IShape adalah tipe dasar dari semua bentuk (karena mereka memiliki metode draw ).

Contoh Java ini cukup sepele, tetapi Anda dapat dengan mudah melihat betapa bergunanya itu dalam aplikasi yang lebih besar di mana pengaturan untuk membuat objek mungkin tidak begitu sederhana. Contohnya adalah video game. Misalkan pengguna harus bertahan dari musuh yang berbeda. Kelas dan antarmuka abstrak dapat digunakan untuk mendefinisikan fungsi inti yang tersedia untuk semua musuh (dan metode yang dapat ditimpa), mungkin menggunakan pola delegasi (mendukung komposisi daripada warisan seperti yang disarankan oleh Geng Empat sehingga Anda tidak terkunci dalam memperpanjang a kelas dasar tunggal dan untuk membuat pengujian/ejekan/DI lebih mudah). Untuk objek musuh yang dipakai dengan cara yang berbeda, antarmuka akan mengizinkan pembuatan objek pabrik sambil mengandalkan tipe antarmuka generik. Ini akan sangat relevan jika musuh diciptakan secara dinamis.

Contoh lain adalah fungsi pembangun. Misalkan kita menggunakan Pola Delegasi agar delegasi kelas bekerja ke kelas lain yang menghormati antarmuka. Kita dapat menempatkan metode build statis pada kelas untuk membuatnya membangun instance-nya sendiri (dengan asumsi Anda tidak menggunakan Wadah/Kerangka Injeksi Ketergantungan). Alih-alih harus memanggil setiap setter, Anda dapat melakukan ini:

 public class User { private IMessagingService msgService; private String name; private int age; public User(String name, int age, IMessagingService msgService) { this.name = name; this.age = age; this.msgService = msgService; } public static User build(String name, int age) { return new User(name, age, new SomeMessageService()); } }

Saya akan menjelaskan Pola Delegasi di artikel selanjutnya jika Anda tidak terbiasa dengannya — pada dasarnya, melalui Komposisi dan dalam hal pemodelan objek, ini menciptakan hubungan "has-a" alih-alih "is-a" hubungan seperti yang akan Anda dapatkan dengan warisan. Jika Anda memiliki kelas Mammal dan kelas Dog , dan Dog memperluas Mammal , maka Dog adalah-a Mammal . Sedangkan, jika Anda memiliki kelas Bark , dan Anda baru saja meneruskan instance Bark ke konstruktor Dog , maka Dog has-a Bark . Seperti yang Anda bayangkan, ini terutama membuat pengujian unit lebih mudah, karena Anda dapat menyuntikkan tiruan dan menegaskan fakta tentang tiruan selama tiruan menghormati kontrak antarmuka di lingkungan pengujian.

Metode pabrik "build" static di atas hanya membuat objek baru User dan meneruskan MessageService yang konkret. Perhatikan bagaimana ini mengikuti definisi di atas — tidak mengekspos logika bisnis untuk membuat objek kelas, atau, dalam hal ini, tidak mengekspos pembuatan layanan pesan ke pemanggil dari pabrik.

Sekali lagi, ini belum tentu bagaimana Anda akan melakukan sesuatu di dunia nyata, tetapi ini menyajikan gagasan fungsi/metode pabrik dengan cukup baik. Kami mungkin menggunakan wadah Injeksi Ketergantungan sebagai gantinya, misalnya. Sekarang kembali ke JavaScript.

Dimulai dengan Ekspres

Express adalah Kerangka Aplikasi Web untuk Node (tersedia melalui Modul NPM) yang memungkinkan seseorang untuk membuat Server Web HTTP. Penting untuk dicatat bahwa Express bukan satu-satunya kerangka kerja untuk melakukan ini (ada Koa, Fastify, dll.), dan bahwa, seperti yang terlihat di artikel sebelumnya, Node dapat berfungsi tanpa Express sebagai entitas yang berdiri sendiri. (Express hanyalah sebuah modul yang dirancang untuk Node — Node dapat melakukan banyak hal tanpanya, meskipun Express populer untuk Server Web).

Sekali lagi, izinkan saya membuat perbedaan yang sangat penting. Ada dikotomi antara Node/JavaScript dan Express. Node, runtime/lingkungan tempat Anda menjalankan JavaScript, dapat melakukan banyak hal — seperti mengizinkan Anda membangun aplikasi React Native, aplikasi desktop, alat baris perintah, dll. — Express tidak lain adalah kerangka kerja ringan yang memungkinkan Anda untuk menggunakan Node/JS untuk membangun server web sebagai lawan dari berurusan dengan jaringan tingkat rendah dan API HTTP Node. Anda tidak perlu Express untuk membangun server web.

Sebelum memulai bagian ini, jika Anda tidak terbiasa dengan HTTP dan Permintaan HTTP (GET, POST, dll.), maka saya mendorong Anda untuk membaca bagian yang sesuai dari artikel saya sebelumnya, yang ditautkan di atas.

Menggunakan Express, kami akan menyiapkan rute yang berbeda untuk membuat Permintaan HTTP, serta titik akhir terkait (yang merupakan fungsi panggilan balik) yang akan diaktifkan saat permintaan dibuat ke rute tersebut. Jangan khawatir jika rute dan titik akhir saat ini tidak masuk akal — saya akan menjelaskannya nanti.

Tidak seperti artikel lain, saya akan mengambil pendekatan menulis kode sumber saat kita pergi, baris demi baris, daripada membuang seluruh basis kode menjadi satu cuplikan dan kemudian menjelaskannya nanti. Mari kita mulai dengan membuka terminal (saya menggunakan Terminus di atas Git Bash di Windows — yang merupakan opsi bagus untuk pengguna Windows yang menginginkan Bash Shell tanpa menyiapkan Subsistem Linux), menyiapkan boilerplate proyek kami, dan membukanya dalam Kode Visual Studio.

 mkdir server && cd server touch server.js npm init -y npm install express code .

Di dalam file server.js , saya akan mulai dengan meminta express menggunakan fungsi require() .

 const express = require('express');

require('express') memberi tahu Node untuk keluar dan mendapatkan modul Express yang kami instal sebelumnya, yang saat ini berada di dalam folder node_modules (untuk itulah yang dilakukan npm install — buat folder node_modules dan letakkan modul dan dependensinya di sana). Dengan konvensi, dan ketika berhadapan dengan Express, kami memanggil variabel yang menyimpan hasil pengembalian dari require('express') express , meskipun itu bisa disebut apa saja.

This returned result, which we have called express , is actually a function — a function we'll have to invoke to create our Express app and set up our routes. Again, by convention, we call this appapp being the return result of express() — that is, the return result of calling the function that has the name express as express() .

 const express = require('express'); const app = express(); // Note that the above variable names are the convention, but not required. // An example such as that below could also be used. const foo = require('express'); const bar = foo(); // Note also that the node module we installed is called express.

The line const app = express(); simply puts a new Express Application inside of the app variable. It calls a function named express (the return result of require('express') ) and stores its return result in a constant named app . If you come from an object-oriented programming background, consider this equivalent to instantiating a new object of a class, where app would be the object and where express() would call the constructor function of the express class. Remember, JavaScript allows us to store functions in variables — functions are first-class citizens. The express variable, then, is nothing more than a mere function. It's provided to us by the developers of Express.

I apologize in advance if I'm taking a very long time to discuss what is actually very basic, but the above, although primitive, confused me quite a lot when I was first learning back-end development with Node.

Inside the Express source code, which is open-source on GitHub, the variable we called express is a function entitled createApplication , which, when invoked, performs the work necessary to create an Express Application:

A snippet of Express source code:

 exports = module.exports = createApplication; /* * Create an express application */ // This is the function we are storing in the express variable. (- Jamie) function createApplication() { // This is what I mean by "Express App" (- Jamie) var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, EventEmitter.prototype, false); mixin(app, proto, false); // expose the prototype that will get set on requests app.request = Object.create(req, { app: { configurable: true, enumerable: true, writable: true, value: app } }) // expose the prototype that will get set on responses app.response = Object.create(res, { app: { configurable: true, enumerable: true, writable: true, value: app } }) app.init(); // See - `app` gets returned. (- Jamie) return app; }

GitHub: https://github.com/expressjs/express/blob/master/lib/express.js

With that short deviation complete, let's continue setting up Express. Thus far, we have required the module and set up our app variable.

 const express = require('express'); const app = express();

From here, we have to tell Express to listen on a port. Any HTTP Requests made to the URL and Port upon which our application is listening will be handled by Express. We do that by calling app.listen(...) , passing to it the port and a callback function which gets called when the server starts running:

 const PORT = 3000; app.listen(PORT, () => console.log(`Server is up on port {PORT}.`));

We notate the PORT variable in capital by convention, for it is a constant variable that will never change. You could do that with all variables that you declare const , but that would look messy. It's up to the developer or development team to decide on notation, so we'll use the above sparsely. I use const everywhere as a method of “defensive coding” — that is, if I know that a variable is never going to change then I might as well just declare it const . Since I define everything const , I make the distinction between what variables should remain the same on a per-request basis and what variables are true actual global constants.

Here is what we have thus far:

 const express = require('express'); const app = express(); const PORT = 3000; // We will build our API here. // ... // Binding our application to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`); });

Let's test this to see if the server starts running on port 3000.

I'll open a terminal and navigate to our project's root directory. I'll then run node server/server.js . Note that this assumes you have Node already installed on your system (You can check with node -v ).

If everything works, you should see the following in the terminal:

Server is up on port 3000.

Go ahead and hit Ctrl + C to bring the server back down.

If this doesn't work for you, or if you see an error such as EADDRINUSE , then it means you may have a service already running on port 3000. Pick another port number, like 3001, 3002, 5000, 8000, etc. Be aware, lower number ports are reserved and there is an upper bound of 65535.

At this point, it's worth taking another small deviation as to understand servers and ports in the context of computer networking. We'll return to Express in a moment. I take this approach, rather than introducing servers and ports first, for the purpose of relevance. That is, it is difficult to learn a concept if you fail to see its applicability. In this way, you are already aware of the use case for ports and servers with Express, so the learning experience will be more pleasurable.

A Brief Look At Servers And Ports

A server is simply a computer or computer program that provides some sort of “functionality” to the clients that talk to it. More generally, it's a device, usually connected to the Internet, that handles connections in a pre-defined manner. In our case, that “pre-defined manner” will be HTTP or the HyperText Transfer Protocol. Servers that use the HTTP Protocol are called Web Servers.

When building an application, the server is a critical component of the “client-server model”, for it permits the sharing and syncing of data (generally via databases or file systems) across devices. It's a cross-platform approach, in a way, for the SDKs of platforms against which you may want to code — be they web, mobile, or desktop — all provide methods (APIs) to interact with a server over HTTP or TCP/UDP Sockets. It's important to make a distinction here — by APIs, I mean programming language constructs to talk to a server, like XMLHttpRequest or the Fetch API in JavaScript, or HttpUrlConnection in Java, or even HttpClient in C#/.NET. This is different from the kind of REST API we'll be building in this article to perform CRUD Operations on a database.

To talk about ports, it's important to understand how clients connect to a server. A client requires the IP Address of the server and the Port Number of our specific service on that server. An IP Address, or Internet Protocol Address, is just an address that uniquely identifies a device on a network. Public and private IPs exist, with private addresses commonly used behind a router or Network Address Translator on a local network. You might see private IP Addresses of the form 192.168.XXX.XXX or 10.0.XXX.XXX . When articulating an IP Address, decimals are called “dots”. So 192.168.0.1 (a common router IP Addr.) might be pronounced, “one nine two dot one six eight dot zero dot one”. (By the way, if you're ever in a hotel and your phone/laptop won't direct you to the AP captive portal, try typing 192.168.0.1 or 192.168.1.1 or similar directly into Chrome).

For simplicity, and since this is not an article about the complexities of computer networking, assume that an IP Address is equivalent to a house address, allowing you to uniquely identify a house (where a house is analogous to a server, client, or network device) in a neighborhood. One neighborhood is one network. Put together all of the neighborhoods in the United States, and you have the public Internet. (This is a basic view, and there are many more complexities — firewalls, NATs, ISP Tiers (Tier One, Tier Two, and Tier Three), fiber optics and fiber optic backbones, packet switches, hops, hubs, etc., subnet masks, etc., to name just a few — in the real networking world.) The traceroute Unix command can provide more insight into the above, displaying the path (and associated latency) that packets take through a network as a series of “hops”.

Nomor Port mengidentifikasi layanan tertentu yang berjalan di server. SSH, atau Secure Shell, yang mengizinkan akses shell jarak jauh ke perangkat, biasanya berjalan pada port 22. FTP atau File Transfer Protocol (yang mungkin, misalnya, digunakan dengan Klien FTP untuk mentransfer aset statis ke server) biasanya berjalan di Pelabuhan 21. Jadi, kita dapat mengatakan bahwa pelabuhan adalah ruangan khusus di dalam setiap rumah dalam analogi kita di atas, karena ruangan di rumah dibuat untuk hal yang berbeda — kamar tidur untuk tidur, dapur untuk menyiapkan makanan, ruang makan untuk konsumsi. makanan, dll., seperti port yang sesuai dengan program yang melakukan layanan tertentu. Bagi kami, Web Server biasanya berjalan pada Port 80, meskipun Anda bebas menentukan Nomor Port mana pun yang Anda inginkan selama tidak digunakan oleh layanan lain (tidak dapat bertabrakan).

Untuk mengakses situs web, Anda memerlukan Alamat IP situs tersebut. Meskipun demikian, kami biasanya mengakses situs web melalui URL. Di balik layar, DNS, atau Server Nama Domain, mengubah URL tersebut menjadi Alamat IP, memungkinkan browser membuat Permintaan GET ke server, mendapatkan HTML, dan menampilkannya ke layar. 8.8.8.8 adalah alamat salah satu Server DNS Publik Google. Anda mungkin membayangkan bahwa memerlukan resolusi nama host ke Alamat IP melalui Server DNS jarak jauh akan memakan waktu, dan Anda benar. Untuk mengurangi latensi, Sistem Operasi memiliki Cache DNS — database sementara yang menyimpan informasi pencarian DNS, sehingga mengurangi frekuensi pencarian tersebut harus dilakukan. DNS Resolver Cache dapat dilihat di Windows dengan perintah ipconfig /displaydns CMD dan dibersihkan melalui perintah ipconfig /flushdns .

Pada Server Unix, port angka rendah yang lebih umum, seperti 80, memerlukan hak akses tingkat root ( ditingkatkan jika Anda berasal dari latar belakang Windows). Untuk alasan itu, kami akan menggunakan port 3000 untuk pekerjaan pengembangan kami, tetapi akan memungkinkan server untuk memilih nomor port (apa pun yang tersedia) ketika kami menyebarkan ke lingkungan produksi kami.

Terakhir, perhatikan bahwa kita dapat mengetik Alamat IP langsung di bilah pencarian Google Chrome, sehingga melewati mekanisme Resolusi DNS. Mengetik 216.58.194.36 , misalnya, akan membawa Anda ke Google.com. Dalam lingkungan pengembangan kami, saat menggunakan komputer kami sendiri sebagai server dev kami, kami akan menggunakan localhost dan port 3000. Alamat diformat sebagai hostname:port , jadi server kami akan berada di localhost:3000 . Localhost, atau 127.0.0.1 , adalah alamat loopback, dan berarti alamat "komputer ini". Ini adalah nama host, dan alamat IPv4-nya berubah menjadi 127.0.0.1 . Coba ping localhost di komputer Anda sekarang. Anda mungkin mendapatkan ::1 kembali — yang merupakan alamat loopback IPv6, atau 127.0.0.1 kembali — yang merupakan alamat loopback IPv4. IPv4 dan IPv6 adalah dua format Alamat IP berbeda yang terkait dengan standar yang berbeda — beberapa alamat IPv6 dapat dikonversi ke IPv4 tetapi tidak semua.

Kembali ke Ekspresi

Saya menyebutkan Permintaan HTTP, Kata Kerja, dan Kode Status di artikel saya sebelumnya, Memulai Dengan Node: Pengantar API, HTTP, dan JavaScript ES6+. Jika Anda tidak memiliki pemahaman umum tentang protokol, jangan ragu untuk melompat ke bagian "Permintaan HTTP dan HTTP" dari bagian itu.

Untuk merasakan Express, kami hanya akan menyiapkan titik akhir kami untuk empat operasi dasar yang akan kami lakukan di database — Buat, Baca, Perbarui, dan Hapus, yang secara kolektif dikenal sebagai CRUD.

Ingat, kami mengakses titik akhir dengan rute di URL. Artinya, meskipun kata "rute" dan "titik akhir" biasanya digunakan secara bergantian, titik akhir secara teknis adalah fungsi bahasa pemrograman (seperti Fungsi Panah ES6) yang melakukan beberapa operasi sisi server, sedangkan rute adalah titik akhir yang terletak di belakang dari . Kami menetapkan titik akhir ini sebagai fungsi panggilan balik, yang akan diaktifkan oleh Express saat permintaan yang sesuai dibuat dari klien ke rute di belakang titik akhir tersebut hidup. Anda dapat mengingat hal di atas dengan menyadari bahwa titik akhir yang menjalankan suatu fungsi dan rute adalah nama yang digunakan untuk mengakses titik akhir. Seperti yang akan kita lihat, rute yang sama dapat dikaitkan dengan beberapa titik akhir dengan menggunakan Kata Kerja HTTP yang berbeda (mirip dengan metode overloading jika Anda berasal dari latar belakang OOP klasik dengan Polimorfisme).

Perlu diingat, kami mengikuti Arsitektur REST (REpresentational State Transfer) dengan mengizinkan klien untuk membuat permintaan ke server kami. Bagaimanapun, ini adalah REST atau RESTful API. Permintaan khusus yang dibuat untuk rute tertentu akan memicu titik akhir tertentu yang akan melakukan hal- hal tertentu. Contoh "hal" yang mungkin dilakukan oleh titik akhir adalah menambahkan data baru ke database, menghapus data, memperbarui data, dll.

Express mengetahui titik akhir mana yang akan diaktifkan karena kami memberi tahunya, secara eksplisit, metode permintaan (GET, POST, dll.) dan rute — kami mendefinisikan fungsi apa yang akan diaktifkan untuk kombinasi spesifik di atas, dan klien membuat permintaan, dengan menentukan rute dan metode. Sederhananya, dengan Node, kami akan memberi tahu Express — “Hei, jika seseorang membuat Permintaan GET untuk rute ini, lanjutkan dan jalankan fungsi ini (gunakan titik akhir ini)”. Hal-hal bisa menjadi lebih rumit: “Express, jika seseorang membuat Permintaan GET untuk rute ini , tetapi mereka tidak mengirimkan Token Pembawa Otorisasi yang valid di header permintaan mereka, maka harap tanggapi dengan HTTP 401 Unauthorized . Jika mereka memiliki Token Pembawa yang valid, silakan kirimkan sumber daya terlindungi apa pun yang mereka cari dengan menembakkan titik akhir. Terima kasih banyak dan semoga harimu menyenangkan.” Memang, alangkah baiknya jika bahasa pemrograman bisa setinggi itu tanpa membocorkan ambiguitas, tetapi tetap menunjukkan konsep dasarnya.

Ingat, titik akhir, di satu sisi, hidup di belakang rute. Jadi sangat penting bahwa klien menyediakan, di header permintaan, metode apa yang ingin digunakan sehingga Express dapat mengetahui apa yang harus dilakukan. Permintaan akan dibuat ke rute tertentu, yang akan ditentukan klien (bersama dengan jenis permintaan) saat menghubungi server, memungkinkan Express melakukan apa yang perlu dilakukan dan kami melakukan apa yang perlu kami lakukan saat Express mengaktifkan callback kami . Itulah yang semuanya bermuara pada.

Dalam contoh kode sebelumnya, kami memanggil fungsi listen yang tersedia di app , meneruskannya ke port dan panggilan balik. app itu sendiri, jika Anda ingat, adalah hasil pengembalian dari pemanggilan variabel express sebagai fungsi (yaitu, express() ), dan variabel express adalah apa yang kami beri nama hasil pengembalian dari persyaratan 'express' dari folder node_modules kami. Sama seperti listen dipanggil di app , kami menentukan Titik Akhir Permintaan HTTP dengan memanggilnya di app . Mari kita lihat GET:

 app.get('/my-test-route', () => { // ... });

Parameter pertama adalah string , dan itu adalah rute di mana titik akhir akan hidup. Fungsi panggilan balik adalah titik akhir. Saya akan mengatakannya lagi: fungsi panggilan balik — parameter kedua — adalah titik akhir yang akan diaktifkan ketika Permintaan GET HTTP dibuat ke rute apa pun yang kami tentukan sebagai argumen pertama ( /my-test-route dalam kasus ini).

Sekarang, sebelum kita melakukan pekerjaan lagi dengan Express, kita perlu mengetahui cara kerja rute. Rute yang kita tentukan sebagai string akan dipanggil dengan membuat permintaan ke www.domain.com/the-route-we-chose-earlier-as-a-string . Dalam kasus kami, domainnya adalah localhost:3000 , yang berarti, untuk mengaktifkan fungsi panggilan balik di atas, kami harus membuat Permintaan GET ke localhost:3000/my-test-route . Jika kita menggunakan string yang berbeda seperti argumen pertama di atas, URL harus berbeda agar sesuai dengan apa yang kita tentukan dalam JavaScript.

Ketika berbicara tentang hal-hal seperti itu, Anda mungkin akan mendengar tentang Pola Glob. Kita dapat mengatakan bahwa semua rute API kita terletak di localhost:3000/** Pola Glob, di mana ** adalah wildcard yang berarti direktori atau sub-direktori apa pun (perhatikan bahwa rute bukanlah direktori) yang root adalah induknya — yaitu, semuanya.

Mari kita lanjutkan dan tambahkan pernyataan log ke fungsi panggilan balik itu sehingga secara keseluruhan kita memiliki:

 // Getting the module from node_modules. const express = require('express'); // Creating our Express Application. const app = express(); // Defining the port we'll bind to. const PORT = 3000; // Defining a new endpoint behind the "/my-test-route" route. app.get('/my-test-route', () => { console.log('A GET Request was made to /my-test-route.'); }); // Binding the server to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`) });

Kami akan mengaktifkan dan menjalankan server kami dengan mengeksekusi node server/server.js (dengan Node terinstal di sistem kami dan dapat diakses secara global dari variabel lingkungan sistem) di direktori root proyek. Seperti sebelumnya, Anda akan melihat pesan bahwa server aktif di konsol. Sekarang server sedang berjalan, buka browser, dan kunjungi localhost:3000 di bilah URL.

Anda akan disambut dengan pesan kesalahan yang menyatakan Cannot GET / . Tekan Ctrl + Shift + I pada Windows di Chrome untuk melihat konsol pengembang. Di sana, Anda akan melihat bahwa kami memiliki 404 (Sumber daya tidak ditemukan). Itu masuk akal — kami hanya memberi tahu server apa yang harus dilakukan ketika seseorang mengunjungi localhost:3000/my-test-route . Browser tidak memiliki apa pun untuk dirender di localhost:3000 (yang setara dengan localhost:3000/ dengan garis miring).

Jika Anda melihat jendela terminal tempat server berjalan, seharusnya tidak ada data baru. Sekarang, kunjungi localhost:3000/my-test-route di bilah URL browser Anda. Anda mungkin melihat kesalahan yang sama di Konsol Chrome (karena browser menyimpan konten dalam cache dan masih tidak memiliki HTML untuk dirender), tetapi jika Anda melihat terminal tempat proses server berjalan, Anda akan melihat bahwa fungsi panggilan balik memang diaktifkan dan pesan log memang dicatat.

Matikan server dengan Ctrl + C.

Sekarang, mari berikan browser sesuatu untuk dirender ketika Permintaan GET dibuat ke rute itu sehingga kita bisa kehilangan pesan Cannot GET / . Saya akan mengambil app.get() kami dari sebelumnya, dan dalam fungsi callback, saya akan menambahkan dua argumen. Ingat, fungsi panggilan balik yang kita lewati dipanggil oleh Express di belakang layar, dan Express dapat menambahkan argumen apa pun yang diinginkannya. Ini sebenarnya menambahkan dua (baik, secara teknis tiga, tetapi kita akan melihatnya nanti), dan meskipun keduanya sangat penting, kami tidak peduli dengan yang pertama untuk saat ini. Argumen kedua disebut res , kependekan dari response , dan saya akan mengaksesnya dengan menetapkan undefined sebagai parameter pertama:

 app.get('/my-test-route', (undefined, res) => { console.log('A GET Request was made to /my-test-route.'); });

Sekali lagi, kita dapat memanggil argumen res apa pun yang kita inginkan, tetapi res adalah konvensi ketika berhadapan dengan Express. res sebenarnya adalah sebuah objek, dan di atasnya ada metode berbeda untuk mengirim data kembali ke klien. Dalam hal ini, saya akan mengakses fungsi send(...) yang tersedia di res untuk mengirim kembali HTML yang akan dirender oleh browser. Kami tidak terbatas untuk mengirim kembali HTML, namun, dan dapat memilih untuk mengirim kembali teks, Objek JavaScript, aliran (aliran sangat indah), atau apa pun.

 app.get('/my-test-route', (undefined, res) => { console.log('A GET Request was made to /my-test-route.'); res.send('<h1>Hello, World!</h1>'); });

Jika Anda mematikan server dan menghidupkannya kembali, lalu menyegarkan browser Anda di rute /my-test-route route, Anda akan melihat HTML dirender.

Tab Jaringan pada Alat Pengembang Chrome akan memungkinkan Anda untuk melihat Permintaan GET ini dengan lebih detail karena berkaitan dengan header.

Pada titik ini, ada baiknya kita mulai belajar tentang Express Middleware — fungsi yang dapat diaktifkan secara global setelah klien membuat permintaan.

Ekspres Middleware

Express menyediakan metode yang digunakan untuk mendefinisikan middleware khusus untuk aplikasi Anda. Memang, arti Express Middleware paling baik didefinisikan dalam Express Docs, di sini)

Fungsi middleware adalah fungsi yang memiliki akses ke objek request ( req ), objek respon ( res ), dan fungsi middleware berikutnya dalam siklus request-response aplikasi. Fungsi middleware berikutnya biasanya dilambangkan dengan variabel bernama next .

Fungsi middleware dapat melakukan tugas-tugas berikut:

  • Jalankan kode apa pun.
  • Buat perubahan pada permintaan dan objek respons.
  • Akhiri siklus permintaan-tanggapan.
  • Panggil fungsi middleware berikutnya di tumpukan.

Dengan kata lain, fungsi middleware adalah fungsi kustom yang dapat kami (pengembang) definisikan, dan yang akan bertindak sebagai perantara antara saat Express menerima permintaan dan saat fungsi panggilan balik kami yang sesuai diaktifkan. Kami mungkin membuat fungsi log , misalnya, yang akan mencatat setiap kali permintaan dibuat. Perhatikan bahwa kita juga dapat memilih untuk mengaktifkan fungsi middleware ini setelah titik akhir kita diaktifkan, tergantung di mana Anda meletakkannya di tumpukan — sesuatu yang akan kita lihat nanti.

Untuk menentukan middleware khusus, kita harus mendefinisikannya sebagai fungsi dan meneruskannya ke app.use(...) .

 const myMiddleware = (req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); } app.use(myMiddleware); // This is the app variable returned from express().

Semua bersama-sama, kita sekarang memiliki:

 // Getting the module from node_modules. const express = require('express'); // Creating our Express Application. const app = express(); // Our middleware function. const myMiddleware = (req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); } // Tell Express to use the middleware. app.use(myMiddleware); // Defining the port we'll bind to. const PORT = 3000; // Defining a new endpoint behind the "/my-test-route" route. app.get('/my-test-route', () => { console.log('A GET Request was made to /my-test-route.'); }); // Binding the server to port 3000. app.listen(PORT, () => { console.log(`Server is up on port ${PORT}.`) });

Jika Anda membuat permintaan melalui browser lagi, Anda sekarang akan melihat bahwa fungsi middleware Anda aktif dan mencatat cap waktu. Untuk mendorong eksperimen, coba hapus panggilan ke fungsi next dan lihat apa yang terjadi.

Fungsi callback middleware dipanggil dengan tiga argumen, req , res , dan next . req adalah parameter yang kita lewati saat membangun GET Handler sebelumnya, dan itu adalah objek yang berisi informasi mengenai permintaan, seperti header, header kustom, parameter, dan badan apa pun yang mungkin telah dikirim dari klien (seperti Anda lakukan dengan Permintaan POST). Saya tahu kita berbicara tentang middleware di sini, tetapi titik akhir dan fungsi middleware dipanggil dengan req dan res . req dan res akan sama (kecuali satu atau yang lain mengubahnya) di middleware dan titik akhir dalam lingkup satu permintaan dari klien. Itu berarti, misalnya, Anda dapat menggunakan fungsi middleware untuk membersihkan data dengan menghapus karakter apa pun yang mungkin ditujukan untuk melakukan SQL atau Injeksi NoSQL, dan kemudian menyerahkan req aman ke titik akhir.

res , seperti yang terlihat sebelumnya, memungkinkan Anda mengirim data kembali ke klien dalam beberapa cara berbeda.

next adalah fungsi panggilan balik yang harus Anda jalankan ketika middleware telah selesai melakukan tugasnya untuk memanggil fungsi middleware berikutnya di tumpukan atau titik akhir. Pastikan untuk mencatat bahwa Anda harus memanggil ini di blok fungsi asinkron then pun yang Anda aktifkan di middleware. Bergantung pada operasi asinkron Anda, Anda mungkin ingin atau tidak ingin memanggilnya di blok catch . Artinya, fungsi myMiddleware diaktifkan setelah permintaan dibuat dari klien tetapi sebelum fungsi titik akhir permintaan diaktifkan. Saat kami mengeksekusi kode ini dan membuat permintaan, Anda akan melihat pesan Middleware has fired... sebelum A GET Request was made to... pesan di konsol. Jika Anda tidak memanggil next() , bagian terakhir tidak akan pernah berjalan — fungsi titik akhir Anda ke permintaan tidak akan diaktifkan.

Perhatikan juga bahwa saya dapat mendefinisikan fungsi ini secara anonim, seperti itu (konvensi yang akan saya pertahankan):

 app.use((req, res, next) => { console.log(`Middleware has fired at time ${Date().now}`); next(); });

Bagi siapa pun yang baru mengenal JavaScript dan ES6, jika cara kerja di atas tidak langsung masuk akal, contoh di bawah ini akan membantu. Kami hanya mendefinisikan fungsi panggilan balik (fungsi anonim) yang mengambil fungsi panggilan balik lain ( next ) sebagai argumen. Kami memanggil fungsi yang mengambil argumen fungsi sebagai Fungsi Orde Tinggi. Lihat di bawah ini — ini menggambarkan contoh dasar bagaimana Kode Sumber Ekspres dapat bekerja di belakang layar:

 console.log('Suppose a request has just been made from the client.\n'); // This is what (it's not exactly) the code behind app.use() might look like. const use = callback => { // Simple log statement to see where we are. console.log('Inside use() - the "use" function has been called.'); // This depicts the termination of the middleware. const next = () => console.log('Terminating Middleware!\n'); // Suppose req and res are defined above (Express provides them). const req = res = null; // "callback" is the "middleware" function that is passed into "use". // "next" is the above function that pretends to stop the middleware. callback(req, res, next); }; // This is analogous to the middleware function we defined earlier. // It gets passed in as "callback" in the "use" function above. const myMiddleware = (req, res, next) => { console.log('Inside the myMiddleware function!'); next(); } // Here, we are actually calling "use()" to see everything work. use(myMiddleware); console.log('Moving on to actually handle the HTTP Request or the next middleware function.');

Kami pertama-tama memanggil use yang menggunakan myMiddleware sebagai argumen. myMiddleware , dengan sendirinya, adalah fungsi yang mengambil tiga argumen - req , res , dan next . Di dalam use , myMiddlware dipanggil, dan ketiga argumen tersebut diteruskan. next adalah fungsi yang didefinisikan di use . myMiddleware didefinisikan sebagai callback dalam metode use . Jika saya menempatkan use , dalam contoh ini, pada objek bernama app , kita dapat meniru pengaturan Express sepenuhnya, meskipun tanpa soket atau konektivitas jaringan.

Dalam hal ini, myMiddleware dan callback adalah Fungsi Tingkat Tinggi, karena keduanya menggunakan fungsi sebagai argumen.

Jika Anda menjalankan kode ini, Anda akan melihat respons berikut:

 Suppose a request has just been made from the client. Inside use() - the "use" function has been called. Inside the middleware function! Terminating Middleware! Moving on to actually handle the HTTP Request or the next middleware function.

Perhatikan bahwa saya juga bisa menggunakan fungsi anonim untuk mencapai hasil yang sama:

 console.log('Suppose a request has just been made from the client.'); // This is what (it's not exactly) the code behind app.use() might look like. const use = callback => { // Simple log statement to see where we are. console.log('Inside use() - the "use" function has been called.'); // This depicts the termination of the middlewear. const next = () => console.log('Terminating Middlewear!'); // Suppose req and res are defined above (Express provides them). const req = res = null; // "callback" is the function which is passed into "use". // "next" is the above function that pretends to stop the middlewear. callback(req, res, () => { console.log('Terminating Middlewear!'); }); }; // Here, we are actually calling "use()" to see everything work. use((req, res, next) => { console.log('Inside the middlewear function!'); next(); }); console.log('Moving on to actually handle the HTTP Request.');

Dengan mudah-mudahan diselesaikan, sekarang kita dapat kembali ke tugas sebenarnya — menyiapkan middleware kita.

Faktanya adalah, Anda biasanya harus mengirim data melalui Permintaan HTTP. Anda memiliki beberapa opsi berbeda untuk melakukannya — mengirim Parameter Kueri URL, mengirimkan data yang akan dapat diakses pada objek req yang telah kita pelajari sebelumnya, dll. Objek itu tidak hanya tersedia dalam panggilan balik untuk memanggil app.use() , tetapi juga ke titik akhir mana pun. Kami menggunakan undefined sebagai pengisi sebelumnya sehingga kami dapat fokus pada res untuk mengirim HTML kembali ke klien, tetapi sekarang, kami memerlukan akses ke sana.

 app.use('/my-test-route', (req, res) => { // The req object contains client-defined data that is sent up. // The res object allows the server to send data back down. });

Permintaan HTTP POST mungkin mengharuskan kami mengirim objek tubuh ke server. Jika Anda memiliki formulir di klien, dan Anda mengambil nama pengguna dan email, Anda mungkin akan mengirim data itu ke server di badan permintaan.

Mari kita lihat seperti apa tampilannya di sisi klien:

 <!DOCTYPE html> <html> <body> <form action="https://localhost:3000/email-list" method="POST" > <input type="text" name="nameInput"> <input type="email" name="emailInput"> <input type="submit"> </form> </body> </html>

Di sisi server:

 app.post('/email-list', (req, res) => { // What do we now? // How do we access the values for the user's name and email? });

Untuk mengakses nama dan email pengguna, kita harus menggunakan jenis middleware tertentu. Ini akan menempatkan data pada objek yang disebut body available on req . Body Parser adalah metode populer untuk melakukan ini, tersedia oleh pengembang Express sebagai modul NPM mandiri. Sekarang, Express telah dikemas sebelumnya dengan middlewarenya sendiri untuk melakukan ini, dan kami akan menyebutnya sebagai berikut:

 app.use(express.urlencoded({ extended: true }));

Sekarang kita bisa melakukan:

 app.post('/email-list', (req, res) => { console.log('User Name: ', req.body.nameInput); console.log('User Email: ', req.body.emailInput); });

Semua ini dilakukan adalah mengambil input yang ditentukan pengguna yang dikirim dari klien, dan membuatnya tersedia di objek body req . Perhatikan bahwa pada req.body , kita sekarang memiliki nameInput dan emailInput , yang merupakan nama tag input dalam HTML. Sekarang, data yang ditentukan klien ini harus dianggap berbahaya (tidak pernah, tidak pernah mempercayai klien), dan perlu dibersihkan, tetapi kami akan membahasnya nanti.

Jenis middleware lain yang disediakan oleh express adalah express.json() . express.json digunakan untuk mengemas setiap Payload JSON yang dikirim dalam permintaan dari klien ke req.body , sementara express.urlencoded akan mengemas setiap permintaan yang masuk dengan string, array, atau data Berkode URL lainnya ke req.body . Singkatnya, keduanya memanipulasi req.body , tetapi .json() adalah untuk JSON Payloads dan .urlencoded() untuk, antara lain, Parameter Kueri POST.

Cara lain untuk mengatakan ini adalah bahwa permintaan yang masuk dengan header Content-Type: application/json (seperti menentukan POST Body dengan fetch API) akan ditangani oleh express.json() , sedangkan permintaan dengan header Content-Type: application/x-www-form-urlencoded (seperti Formulir HTML) akan ditangani dengan express.urlencoded() . Ini mudah-mudahan sekarang masuk akal.

Memulai Rute CRUD Kami Untuk MongoDB

Catatan : Saat melakukan Permintaan PATCH di artikel ini, kami tidak akan mengikuti Spesifikasi RFC JSONPatch — masalah yang akan kami perbaiki di artikel berikutnya dari seri ini.

Mengingat bahwa kami memahami bahwa kami menentukan setiap titik akhir dengan memanggil fungsi yang relevan pada app , meneruskan rute dan fungsi panggilan balik yang berisi objek permintaan dan respons, kami dapat mulai mendefinisikan Rute CRUD kami untuk API Rak Buku. Memang, dan mengingat ini adalah artikel pengantar, saya tidak akan berhati-hati untuk mengikuti spesifikasi HTTP dan REST sepenuhnya, saya juga tidak akan mencoba menggunakan arsitektur yang paling bersih. Itu akan datang di artikel mendatang.

Saya akan membuka file server.js yang telah kita gunakan sejauh ini dan mengosongkan semuanya untuk memulai dari clean slate di bawah ini:

 // Getting the module from node_modules. const express = require('express'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true )); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // ... // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Pertimbangkan semua kode berikut untuk mengambil bagian // ... dari file di atas.

Untuk menentukan titik akhir kita, dan karena kita sedang membangun REST API, kita harus mendiskusikan cara yang tepat untuk memberi nama rute. Sekali lagi, Anda harus melihat bagian HTTP dari artikel saya sebelumnya untuk informasi lebih lanjut. Kami berurusan dengan buku, jadi semua rute akan ditempatkan di belakang /books (konvensi penamaan jamak adalah standar).

Meminta Rute
POS /books
DAPATKAN /books/id
PATCH /books/id
MENGHAPUS /books/id

Seperti yang Anda lihat, ID tidak perlu ditentukan saat POSTing buku karena kami akan (atau lebih tepatnya, MongoDB), akan membuatnya untuk kami, secara otomatis, di sisi server. MENDAPATKAN, PATCHING, dan MENGHAPUS semua buku akan mengharuskan kita meneruskan ID itu ke titik akhir kita, yang akan kita bahas nanti. Untuk saat ini, mari kita buat titik akhir:

 // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); });

Sintaks :id memberi tahu Express bahwa id adalah parameter dinamis yang akan dilewatkan di URL. Kami memiliki akses ke objek params yang tersedia di req . Saya tahu "kami memiliki akses ke sana di req " terdengar seperti sihir dan sihir (yang tidak ada) berbahaya dalam pemrograman, tetapi Anda harus ingat bahwa Express bukan kotak hitam. Ini adalah proyek sumber terbuka yang tersedia di GitHub di bawah Lisensi MIT. Anda dapat dengan mudah melihat kode sumbernya jika Anda ingin melihat bagaimana parameter kueri dinamis dimasukkan ke objek req .

Secara keseluruhan, kami sekarang memiliki yang berikut di file server.js kami:

 // Getting the module from node_modules. const express = require('express'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Lanjutkan dan mulai server, jalankan node server.js dari terminal atau baris perintah, dan kunjungi browser Anda. Buka Konsol Pengembangan Chrome, dan di Bilah URL (Uniform Resource Locator), kunjungi localhost:3000/books . Anda seharusnya sudah melihat indikator di terminal OS Anda bahwa server sudah aktif serta pernyataan log untuk GET.

Sejauh ini, kami telah menggunakan browser web untuk melakukan GET Requests. Itu bagus untuk memulai, tetapi kami akan segera menemukan bahwa ada alat yang lebih baik untuk menguji rute API. Memang, kami dapat menempelkan panggilan fetch langsung ke konsol atau menggunakan beberapa layanan online. Dalam kasus kami, dan untuk menghemat waktu, kami akan menggunakan cURL dan Postman. Saya menggunakan keduanya dalam artikel ini (walaupun Anda dapat menggunakan salah satu atau) sehingga saya dapat memperkenalkannya jika Anda belum menggunakannya. cURL adalah perpustakaan (perpustakaan yang sangat, sangat penting) dan alat baris perintah yang dirancang untuk mentransfer data menggunakan berbagai protokol. Postman adalah alat berbasis GUI untuk menguji API. Setelah mengikuti petunjuk penginstalan yang relevan untuk kedua alat di sistem operasi Anda, pastikan server Anda masih berjalan, lalu jalankan perintah berikut (satu per satu) di terminal baru. Anda harus mengetiknya dan menjalankannya satu per satu, lalu menonton pesan log di terminal terpisah dari server Anda. Juga, perhatikan bahwa simbol komentar bahasa pemrograman standar // bukan simbol yang valid di Bash atau MS-DOS. Anda harus menghilangkan baris tersebut, dan saya hanya menggunakannya di sini untuk menjelaskan setiap blok perintah cURL .

 // HTTP POST Request (Localhost, IPv4, IPv6) curl -X POST https://localhost:3000/books curl -X POST https://127.0.0.1:3000/books curl -X POST https://[::1]:3000/books // HTTP GET Request (Localhost, IPv4, IPv6) curl -X GET https://localhost:3000/books/123abc curl -X GET https://127.0.0.1:3000/books/book-id-123 curl -X GET https://[::1]:3000/books/book-abc123 // HTTP PATCH Request (Localhost, IPv4, IPv6) curl -X PATCH https://localhost:3000/books/456 curl -X PATCH https://127.0.0.1:3000/books/218 curl -X PATCH https://[::1]:3000/books/some-id // HTTP DELETE Request (Localhost, IPv4, IPv6) curl -X DELETE https://localhost:3000/books/abc curl -X DELETE https://127.0.0.1:3000/books/314 curl -X DELETE https://[::1]:3000/books/217

Seperti yang Anda lihat, ID yang diteruskan sebagai Parameter URL dapat berupa nilai apa pun. Bendera -X menentukan jenis Permintaan HTTP (dapat dihilangkan untuk GET), dan kami menyediakan URL yang akan digunakan untuk membuat permintaan setelahnya. Saya telah menggandakan setiap permintaan tiga kali, memungkinkan Anda untuk melihat bahwa semuanya masih berfungsi baik Anda menggunakan nama host localhost , Alamat IPv4 ( 127.0.0.1 ) yang diselesaikan oleh localhost , atau Alamat IPv6 ( ::1 ) yang diselesaikan oleh localhost . Perhatikan bahwa cURL memerlukan pembungkusan Alamat IPv6 dalam tanda kurung siku.

Kami berada di tempat yang layak sekarang — kami memiliki struktur sederhana dari rute dan titik akhir kami. Server berjalan dengan benar dan menerima Permintaan HTTP seperti yang kami harapkan. Bertentangan dengan apa yang Anda harapkan, tidak ada waktu lama untuk pergi pada saat ini — kita hanya perlu menyiapkan database kita, menghostingnya (menggunakan Database-as-a-Service — MongoDB Atlas), dan menyimpan data ke dalamnya (dan melakukan validasi dan membuat respons kesalahan).

Menyiapkan Basis Data MongoDB Produksi

Untuk menyiapkan basis data produksi, kita akan menuju ke Halaman Beranda Atlas MongoDB dan mendaftar untuk mendapatkan akun gratis. Setelah itu, buat cluster baru. Anda dapat mempertahankan pengaturan default, memilih wilayah yang berlaku tingkat biaya. Kemudian tekan tombol "Buat Cluster". Cluster akan membutuhkan waktu untuk dibuat, dan kemudian Anda akan dapat memperoleh URL database dan kata sandi Anda. Perhatikan ini ketika Anda melihatnya. Kami akan mengkodekannya untuk saat ini, dan kemudian menyimpannya dalam variabel lingkungan nanti untuk tujuan keamanan. Untuk bantuan dalam membuat dan menghubungkan ke cluster, saya akan merujuk Anda ke Dokumentasi MongoDB, khususnya halaman ini dan halaman ini, atau Anda dapat meninggalkan komentar di bawah dan saya akan mencoba membantu.

Membuat Model Luwak

Anda disarankan untuk memahami arti dari Dokumen dan Koleksi dalam konteks NoSQL (Not Only SQL — Structured Query Language). Untuk referensi, Anda mungkin ingin membaca Panduan Mulai Cepat Mongoose dan bagian MongoDB dari artikel saya sebelumnya.

Kami sekarang memiliki database yang siap menerima Operasi CRUD. Mongoose adalah modul Node (atau ODM — Object Document Mapper) yang akan memungkinkan kita untuk melakukan operasi tersebut (mengabstraksi beberapa kerumitan) serta mengatur skema, atau struktur, dari kumpulan database.

Sebagai penafian penting, ada banyak kontroversi seputar ORM dan pola seperti Rekaman Aktif atau Pemeta Data. Beberapa pengembang bersumpah dengan ORM dan yang lain bersumpah menentang mereka (percaya bahwa mereka menghalangi). Penting juga untuk dicatat bahwa ORM sangat jauh seperti penggabungan koneksi, koneksi soket, dan penanganan, dll. Anda dapat dengan mudah menggunakan Driver Asli MongoDB (Modul NPM lain), tetapi itu akan berbicara lebih banyak pekerjaan. Meskipun disarankan agar Anda bermain dengan Native Driver sebelum menggunakan ORM, saya menghilangkan Native Driver di sini untuk singkatnya. Untuk operasi SQL yang kompleks pada Database Relasional, tidak semua ORM akan dioptimalkan untuk kecepatan kueri, dan Anda mungkin akhirnya menulis SQL mentah Anda sendiri. ORMs can come into play a lot with Domain-Driven Design and CQRS, among others. They are an established concept in the .NET world, and the Node.js community has not completely caught up yet — TypeORM is better, but it's not NHibernate or Entity Framework.

To create our Model, I'll create a new folder in the server directory entitled models , within which I'll create a single file with the name book.js . Thus far, our project's directory structure is as follows:

 - server - node_modules - models - book.js - package.json - server.js

Indeed, this directory structure is not required, but I use it here because it's simple. Allow me to note that this is not at all the kind of architecture you want to use for larger applications (and you might not even want to use JavaScript — TypeScript could be a better option), which I discuss in this article's closing. The next step will be to install mongoose , which is performed via, as you might expect, npm i mongoose .

The meaning of a Model is best ascertained from the Mongoose documentation:

Models are fancy constructors compiled from Schema definitions. An instance of a model is called a document. Models are responsible for creating and reading documents from the underlying MongoDB database.

Before creating the Model, we'll define its Schema. A Schema will, among others, make certain expectations about the value of the properties provided. MongoDB is schemaless, and thus this functionality is provided by the Mongoose ODM. Let's start with a simple example. Suppose I want my database to store a user's name, email address, and password. Traditionally, as a plain old JavaScript Object (POJO), such a structure might look like this:

 const userDocument = { name: 'Jamie Corkhill', email: '[email protected]', password: 'Bcrypt Hash' };

If that above object was how we expected our user's object to look, then we would need to define a schema for it, like this:

 const schema = { name: { type: String, trim: true, required: true }, email: { type: String, trim: true, required: true }, password: { type: String, required: true } };

Notice that when creating our schema, we define what properties will be available on each document in the collection as an object in the schema. In our case, that's name , email , and password . The fields type , trim , required tell Mongoose what data to expect. If we try to set the name field to a number, for example, or if we don't provide a field, Mongoose will throw an error (because we are expecting a type of String ), and we can send back a 400 Bad Request to the client. This might not make sense right now because we have defined an arbitrary schema object. However, the fields of type , trim , and required (among others) are special validators that Mongoose understands. trim , for example, will remove any whitespace from the beginning and end of the string. We'll pass the above schema to mongoose.Schema() in the future and that function will know what to do with the validators.

Understanding how Schemas work, we'll create the model for our Books Collection of the Bookshelf API. Let's define what data we require:

  1. Judul

  2. ISBN Number

  3. Pengarang

    1. Nama depan

    2. Nama keluarga

  4. Publishing Date

  5. Finished Reading (Boolean)

I'm going to create this in the book.js file we created earlier in /models . Like the example above, we'll be performing validation:

 const mongoose = require('mongoose'); // Define the schema: const mySchema = { title: { type: String, required: true, trim: true, }, isbn: { type: String, required: true, trim: true, }, author: { firstName:{ type: String, required: true, trim: true }, lastName: { type: String, required: true, trim: true } }, publishingDate: { type: String }, finishedReading: { type: Boolean, required: true, default: false } }

default will set a default value for the property if none is provided — finishedReading for example, although a required field, will be set automatically to false if the client does not send one up.

Mongoose also provides the ability to perform custom validation on our fields, which is done by supplying the validate() method, which attains the value that was attempted to be set as its one and only parameter. In this function, we can throw an error if the validation fails. Berikut ini contohnya:

 // ... isbn: { type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } } // ...

Now, if anyone supplies an invalid ISBN to our model, Mongoose will throw an error when trying to save that document to the collection. I've already installed the NPM module validator via npm i validator and required it. validator contains a bunch of helper functions for common validation requirements, and I use it here instead of RegEx because ISBNs can't be validated with RegEx alone due to a tailing checksum. Remember, users will be sending a JSON body to one of our POST routes. That endpoint will catch any errors (such as an invalid ISBN) when attempting to save, and if one is thrown, it'll return a blank response with an HTTP 400 Bad Request status — we haven't yet added that functionality.

Finally, we have to define our schema of earlier as the schema for our model, so I'll make a call to mongoose.Schema() passing in that schema:

 const bookSchema = mongoose.Schema(mySchema);

To make things more precise and clean, I'll replace the mySchema variable with the actual object all on one line:

 const bookSchema = mongoose.Schema({ title:{ type: String, required: true, trim: true, }, isbn:{ type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } }, author:{ firstName: { type: String required: true, trim: true }, lastName:{ type: String, required: true, trim: true } }, publishingDate:{ type: String }, finishedReading:{ type: Boolean, required: true, default: false } });

Let's take a final moment to discuss this schema. We are saying that each of our documents will consist of a title, an ISBN, an author with a first and last name, a publishing date, and a finishedReading boolean.

  1. title will be of type String , it's a required field, and we'll trim any whitespace.
  2. isbn will be of type String , it's a required field, it must match the validator, and we'll trim any whitespace.
  3. author is of type object containing a required, trimmed, string firstName and a required, trimmed, string lastName.
  4. publishingDate is of type String (although we could make it of type Date or Number for a Unix timestamp.
  5. finishedReading is a required boolean that will default to false if not provided.

With our bookSchema defined, Mongoose knows what data and what fields to expect within each document to the collection that stores books. However, how do we tell it what collection that specific schema defines? We could have hundreds of collections, so how do we correlate, or tie, bookSchema to the Book collection?

The answer, as seen earlier, is with the use of models. We'll use bookSchema to create a model, and that model will model the data to be stored in the Book collection, which will be created by Mongoose automatically.

Append the following lines to the end of the file:

 const Book = mongoose.model('Book', bookSchema); module.exports = Book;

As you can see, we have created a model, the name of which is Book (— the first parameter to mongoose.model() ), and also provided the ruleset, or schema, to which all data is saved in the Book collection will have to abide. We export this model as a default export, allowing us to require the file for our endpoints to access. Book is the object upon which we'll call all of the required functions to Create, Read, Update, and Delete data which are provided by Mongoose.

Altogether, our book.js file should look as follows:

 const mongoose = require('mongoose'); const validator = require('validator'); // Define the schema. const bookSchema = mongoose.Schema({ title:{ type: String, required: true, trim: true, }, isbn:{ type: String, required: true, trim: true, validate(value) { if (!validator.isISBN(value)) { throw new Error('ISBN is invalid.'); } } }, author:{ firstName: { type: String, required: true, trim: true }, lastName:{ type: String, required: true, trim: true } }, publishingDate:{ type: String }, finishedReading:{ type: Boolean, required: true, default: false } }); // Create the "Book" model of name Book with schema bookSchema. const Book = mongoose.model('Book', bookSchema); // Provide the model as a default export. module.exports = Book;

Connecting To MongoDB (Basics)

Don't worry about copying down this code. I'll provide a better version in the next section. To connect to our database, we'll have to provide the database URL and password. We'll call the connect method available on mongoose to do so, passing to it the required data. For now, we are going hardcode the URL and password — an extremely frowned upon technique for many reasons: namely the accidental committing of sensitive data to a public (or private made public) GitHub Repository. Realize also that commit history is saved, and that if you accidentally commit a piece of sensitive data, removing it in a future commit will not prevent people from seeing it (or bots from harvesting it), because it's still available in the commit history. CLI tools exist to mitigate this issue and remove history.

As stated, for now, we'll hard code the URL and password, and then save them to environment variables later. At this point, let's look at simply how to do this, and then I'll mention a way to optimize it.

const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false, useUnifiedTopology: true });

Ini akan terhubung ke database. Kami menyediakan URL yang kami peroleh dari dasbor MongoDB Atlas, dan objek yang diteruskan sebagai parameter kedua menentukan fitur yang akan digunakan untuk, antara lain, mencegah peringatan penghentian.

Mongoose, yang menggunakan inti MongoDB Native Driver di belakang layar, harus berusaha mengikuti perubahan yang dibuat pada driver. Dalam versi driver yang baru, mekanisme yang digunakan untuk mengurai URL koneksi telah diubah, jadi kami meneruskan useNewUrlParser: true untuk menentukan bahwa kami ingin menggunakan versi terbaru yang tersedia dari driver resmi.

Secara default, jika Anda menetapkan indeks (dan mereka disebut "indeks" bukan "indeks") (yang tidak akan kami bahas dalam artikel ini) pada data di database Anda, Mongoose menggunakan fungsi ensureIndex() yang tersedia dari Driver Asli. MongoDB tidak lagi menggunakan fungsi itu demi createIndex() , dan dengan demikian menyetel flag useCreateIndex ke true akan memberi tahu Mongoose untuk menggunakan metode createIndex() dari driver, yang merupakan fungsi yang tidak digunakan lagi.

Versi asli findOneAndUpdate dari luwak (yang merupakan metode untuk menemukan dokumen dalam database dan memperbaruinya) mendahului versi Native Driver. Artinya, findOneAndUpdate() pada awalnya bukan fungsi Driver Asli melainkan yang disediakan oleh Mongoose, jadi Mongoose harus menggunakan findAndModify disediakan di belakang layar oleh driver untuk membuat fungsionalitas findOneAndUpdate . Dengan driver yang sekarang diperbarui, itu berisi fungsi seperti itu sendiri, jadi kita tidak perlu menggunakan findAndModify . Ini mungkin tidak masuk akal, dan tidak apa-apa — ini bukan informasi penting tentang skala hal.

Akhirnya, MongoDB menghentikan server lama dan sistem pemantauan mesinnya. Kami menggunakan metode baru dengan useUnifiedTopology: true .

Apa yang kita miliki sejauh ini adalah cara untuk terhubung ke database. Tapi ada satu hal — ini tidak terukur atau efisien. Saat kami menulis pengujian unit untuk API ini, pengujian unit akan menggunakan data pengujian mereka sendiri (atau perlengkapan) pada database pengujian mereka sendiri. Jadi, kami menginginkan cara untuk dapat membuat koneksi untuk tujuan yang berbeda — beberapa untuk lingkungan pengujian (yang dapat kami putar dan hancurkan sesuka hati), yang lain untuk lingkungan pengembangan, dan lainnya untuk lingkungan produksi. Untuk itu, kami akan membangun pabrik. (Ingat itu dari sebelumnya?)

Menghubungkan ke Mongo — Membangun Implementasi Pabrik JS

Memang, Objek Java sama sekali tidak analog dengan Objek JavaScript, dan selanjutnya, apa yang kita ketahui di atas dari Pola Desain Pabrik tidak akan berlaku. Saya hanya memberikan itu sebagai contoh untuk menunjukkan pola tradisional. Untuk mencapai objek di Java, atau C#, atau C++, dll., kita harus membuat instance kelas. Ini dilakukan dengan kata kunci new , yang menginstruksikan kompiler untuk mengalokasikan memori untuk objek di heap. Di C++, ini memberi kita pointer ke objek yang harus kita bersihkan sendiri sehingga kita tidak memiliki pointer gantung atau kebocoran memori (C++ tidak memiliki pengumpul sampah, tidak seperti Node/V8 yang dibangun di atas C++) Dalam JavaScript, di atas tidak perlu dilakukan — kita tidak perlu membuat instance kelas untuk mendapatkan objek — objek hanyalah {} . Beberapa orang akan mengatakan bahwa segala sesuatu dalam JavaScript adalah sebuah objek, meskipun itu secara teknis tidak benar karena tipe primitif bukanlah objek.

Untuk alasan di atas, Pabrik JS kami akan lebih sederhana, berpegang pada definisi longgar dari pabrik menjadi fungsi yang mengembalikan objek (objek JS). Karena suatu fungsi adalah sebuah objek (untuk function diwarisi dari object melalui pewarisan prototipe), contoh kita di bawah ini akan memenuhi kriteria ini. Untuk mengimplementasikan pabrik, saya akan membuat folder baru di dalam server bernama db . Dalam db saya akan membuat file baru bernama mongoose.js . File ini akan membuat koneksi ke database. Di dalam mongoose.js , saya akan membuat fungsi bernama connectionFactory dan mengekspornya secara default:

 // Directory - server/db/mongoose.js const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; const connectionFactory = () => { return mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: false }); }; module.exports = connectionFactory;

Menggunakan singkatan yang disediakan oleh ES6 untuk Fungsi Panah yang mengembalikan satu pernyataan pada baris yang sama dengan tanda tangan metode, saya akan membuat file ini lebih sederhana dengan menghilangkan definisi connectionFactory dan hanya mengekspor pabrik secara default:

 // server/db/mongoose.js const mongoose = require('mongoose'); const MONGODB_URL = 'Your MongoDB URL'; module.exports = () => mongoose.connect(MONGODB_URL, { useNewUrlParser: true, useCreateIndex: true, useFindAndModify: true });

Sekarang, yang harus dilakukan adalah meminta file dan memanggil metode yang diekspor, seperti ini:

 const connectionFactory = require('./db/mongoose'); connectionFactory(); // OR require('./db/mongoose')();

Anda dapat membalikkan kontrol dengan meminta URL MongoDB Anda diberikan sebagai parameter ke fungsi pabrik, tetapi kami akan mengubah URL secara dinamis sebagai variabel lingkungan berdasarkan lingkungan.

Manfaat membuat koneksi kita sebagai suatu fungsi adalah kita dapat memanggil fungsi itu nanti dalam kode untuk terhubung ke database dari file yang ditujukan untuk produksi dan yang ditujukan untuk pengujian integrasi lokal dan jarak jauh baik di perangkat maupun dengan pipa CI/CD jarak jauh /membangun server.

Membangun Titik Akhir Kami

Kami sekarang mulai menambahkan logika terkait CRUD yang sangat sederhana ke titik akhir kami. Seperti yang dinyatakan sebelumnya, penafian singkat sedang dilakukan. Metode yang kami gunakan untuk menerapkan logika bisnis kami di sini bukanlah metode yang harus Anda cerminkan untuk apa pun selain proyek sederhana. Menghubungkan ke database dan menjalankan logika secara langsung di dalam titik akhir adalah (dan seharusnya) tidak disukai, karena Anda kehilangan kemampuan untuk menukar layanan atau DBMS tanpa harus melakukan refactor seluruh aplikasi. Meskipun demikian, mengingat ini adalah artikel pemula, saya menerapkan praktik buruk ini di sini. Artikel mendatang dalam seri ini akan membahas bagaimana kita dapat meningkatkan kompleksitas dan kualitas arsitektur kita.

Untuk saat ini, mari kembali ke file server.js kita dan pastikan kita berdua memiliki titik awal yang sama. Perhatikan saya menambahkan pernyataan yang require untuk pabrik koneksi database kami dan saya mengimpor model yang kami ekspor dari ./models/book.js .

 const express = require('express'); // Database connection and model. require('./db/mongoose.js'); const Book = require('./models/book.js'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', (req, res) => { // ... console.log('A POST Request was made!'); }); // HTTP GET /books/:id app.get('/books/:id', (req, res) => { // ... console.log(`A GET Request was made! Getting book ${req.params.id}`); }); // HTTP PATCH /books/:id app.patch('/books/:id', (req, res) => { // ... console.log(`A PATCH Request was made! Updating book ${req.params.id}`); }); // HTTP DELETE /books/:id app.delete('/books/:id', (req, res) => { // ... console.log(`A DELETE Request was made! Deleting book ${req.params.id}`); }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Saya akan mulai dengan app.post() . Kami memiliki akses ke model Book karena kami mengekspornya dari file tempat kami membuatnya. Sebagaimana dinyatakan dalam dokumen Mongoose, Book dapat dibangun. Untuk membuat buku baru, kami memanggil konstruktor dan meneruskan data buku, sebagai berikut:

 const book = new Book(bookData);

Dalam kasus kami, kami akan memiliki bookData sebagai objek yang dikirim dalam permintaan, yang akan tersedia di req.body.book . Ingat, middleware express.json() akan menempatkan data JSON apa pun yang kami kirim ke req.body . Kami akan mengirimkan JSON dalam format berikut:

 { "book": { "title": "The Art of Computer Programming", "isbn": "ISBN-13: 978-0-201-89683-1", "author": { "firstName": "Donald", "lastName": "Knuth" }, "publishingDate": "July 17, 1997", "finishedReading": true } }

Artinya, JSON yang kita lewati akan diurai, dan seluruh objek JSON (pasangan kurung kurawal pertama) akan ditempatkan di req.body oleh middleware express.json express.json() . Satu-satunya properti di objek JSON kami adalah book , dan dengan demikian objek book akan tersedia di req.body.book .

Pada titik ini, kita dapat memanggil fungsi konstruktor model dan meneruskan data kita:

 app.post('/books', async (req, res) => { // <- Notice 'async' const book = new Book(req.body.book); await book.save(); // <- Notice 'await' });

Perhatikan beberapa hal di sini. Memanggil metode save pada instance yang kita dapatkan kembali dari memanggil fungsi konstruktor akan mempertahankan objek req.body.book ke database jika dan hanya jika sesuai dengan skema yang kita definisikan dalam model Mongoose. Tindakan menyimpan data ke database adalah operasi asinkron, dan metode save() ini mengembalikan sebuah janji — penyelesaian yang sangat kita tunggu. Daripada rantai pada panggilan .then .then() , saya menggunakan sintaks ES6 Async/Await, yang berarti saya harus membuat fungsi panggilan balik ke app.post async .

book.save() akan menolak dengan ValidationError jika objek yang dikirim klien tidak sesuai dengan skema yang kita definisikan. Pengaturan kami saat ini membuat beberapa kode yang sangat tidak stabil dan ditulis dengan buruk, karena kami tidak ingin aplikasi kami mogok jika terjadi kegagalan terkait validasi. Untuk memperbaikinya, saya akan mengelilingi operasi berbahaya dalam klausa try/catch . Jika terjadi kesalahan, saya akan mengembalikan HTTP 400 Bad Request atau HTTP 422 Unprocessable Entity. Ada beberapa perdebatan tentang mana yang harus digunakan, jadi saya akan tetap menggunakan 400 untuk artikel ini karena lebih umum.

 app.post('/books', async (req, res) => { try { const book = new Book(req.body.book); await book.save(); return res.status(201).send({ book }); } catch (e) { return res.status(400).send({ error: 'ValidationError' }); } });

Perhatikan bahwa saya menggunakan ES6 Object Shorthand untuk mengembalikan objek book segera kembali ke klien dalam kasus sukses dengan res.send({ book }) — yang akan setara dengan res.send({ book: book }) . Saya juga mengembalikan ekspresi hanya untuk memastikan fungsi saya keluar. Di blok catch , saya menetapkan status menjadi 400 secara eksplisit, dan mengembalikan string 'ValidationError' pada properti error objek yang dikirim kembali. A 201 adalah kode status jalur sukses yang berarti "DIBUAT".

Memang, ini juga bukan solusi terbaik karena kami tidak dapat memastikan alasan kegagalan adalah Permintaan Buruk dari pihak klien. Mungkin kami kehilangan koneksi (seharusnya koneksi soket terputus, sehingga pengecualian sementara) ke database, dalam hal ini kami mungkin harus mengembalikan kesalahan 500 Server Internal. Cara untuk memeriksa ini adalah dengan membaca objek kesalahan e dan secara selektif mengembalikan respons. Mari kita lakukan sekarang, tetapi seperti yang telah saya katakan berkali-kali, artikel lanjutan akan membahas arsitektur yang tepat dalam hal Router, Pengontrol, Layanan, Repositori, kelas kesalahan khusus, middleware kesalahan khusus, tanggapan kesalahan khusus, Model Database/Data Entitas Domain pemetaan, dan Pemisahan Permintaan Perintah (CQS).

 app.post('/books', async (req, res) => { try { const book = new Book(req.body.book); await book.save(); return res.send({ book }); } catch (e) { if (e instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'ValidationError' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Silakan dan buka Postman (dengan asumsi Anda memilikinya, jika tidak, unduh dan instal) dan buat permintaan baru. Kami akan membuat Permintaan POST ke localhost:3000/books . Di bawah tab "Tubuh" di bagian Permintaan Tukang Pos, saya akan memilih tombol radio "mentah" dan memilih "JSON" di tombol tarik-turun di paling kanan. Ini akan melanjutkan dan secara otomatis menambahkan header Content-Type: application/json ke permintaan. Saya kemudian akan menyalin dan menempelkan Objek JSON Buku dari sebelumnya ke dalam area teks Tubuh. Inilah yang kami miliki:

GUI Tukang Pos diisi dengan data respons dari Permintaan POST.
Respons Payload JSON untuk Permintaan POST kami. (Pratinjau besar)

Setelah itu, saya akan menekan tombol kirim, dan Anda akan melihat 201 Created response di bagian "Response" pada Postman (baris bawah). Kami melihat ini karena kami secara khusus meminta Express untuk merespons dengan objek 201 dan Book — jika kami baru saja melakukan res.send() tanpa kode status, express akan secara otomatis merespons dengan 200 OK. Seperti yang Anda lihat, objek Book sekarang disimpan ke database dan telah dikembalikan ke klien sebagai Response to the POST Request.

GUI Tukang Pos diisi dengan data untuk Permintaan POST.
Data untuk mengisi bidang Tukang Pos untuk Permintaan POST kami. (Pratinjau besar)

Jika Anda melihat database Koleksi Buku melalui MongoDB Atlas, Anda akan melihat bahwa buku tersebut memang disimpan.

Anda juga dapat mengetahui bahwa MongoDB telah memasukkan bidang __v dan _id . Yang pertama mewakili versi dokumen, dalam hal ini, 0, dan yang terakhir adalah ObjectID dokumen — yang secara otomatis dihasilkan oleh MongoDB dan dijamin memiliki kemungkinan tabrakan yang rendah.

Ringkasan Dari Apa yang Telah Kami Bahas Sejauh Ini

Kami telah membahas banyak sejauh ini dalam artikel. Mari kita mengambil penangguhan hukuman singkat dengan membahas ringkasan singkat sebelum kembali untuk menyelesaikan Express API.

Kami mempelajari tentang ES6 Object Destructuring, ES6 Object Shorthand Syntax, serta operator ES6 Rest/Spread. Ketiganya mari kita lakukan hal berikut (dan lebih banyak lagi, seperti yang dibahas di atas):

 // Destructuring Object Properties: const { a: newNameA = 'Default', b } = { a: 'someData', b: 'info' }; console.log(`newNameA: ${newNameA}, b: ${b}`); // newNameA: someData, b: info // Destructuring Array Elements const [elemOne, elemTwo] = [() => console.log('hi'), 'data']; console.log(`elemOne(): ${elemOne()}, elemTwo: ${elemTwo}`); // elemOne(): hi, elemTwo: data // Object Shorthand const makeObj = (name) => ({ name }); console.log(`makeObj('Tim'): ${JSON.stringify(makeObj('Tim'))}`); // makeObj('Tim'): { "name": "Tim" } // Rest, Spread const [c, d, ...rest] = [0, 1, 2, 3, 4]; console.log(`c: ${c}, d: ${d}, rest: ${rest}`) // c: 0, d: 1, rest: 2, 3, 4

Kami juga membahas Express, Expess Middleware, Servers, Ports, IP Addressing, dll. Hal-hal menjadi menarik ketika kami mengetahui bahwa ada metode yang tersedia pada hasil pengembalian dari require('express')(); dengan nama-nama Kata Kerja HTTP, seperti app.get dan app.post .

Jika bagian require('express')() itu tidak masuk akal bagi Anda, inilah poin yang saya buat:

 const express = require('express'); const app = express(); app.someHTTPVerb

Seharusnya masuk akal dengan cara yang sama seperti kita melepaskan pabrik koneksi sebelumnya untuk Mongoose.

Setiap pengendali rute, yang merupakan fungsi titik akhir (atau fungsi panggilan balik), dilewatkan dalam objek req dan objek res dari Express di belakang layar. (Mereka secara teknis juga mendapatkan next , seperti yang akan kita lihat sebentar lagi). req berisi data khusus untuk permintaan masuk dari klien, seperti header atau JSON apa pun yang dikirim. res adalah apa yang memungkinkan kami untuk mengembalikan tanggapan kepada klien. Fungsi next juga diteruskan ke penangan.

Dengan Mongoose, kami melihat bagaimana kami dapat terhubung ke database dengan dua metode — cara primitif dan cara yang lebih maju/praktis yang meminjam dari Pola Pabrik. Kami akhirnya akan menggunakan ini ketika kami membahas Pengujian Unit dan Integrasi dengan Jest (dan pengujian mutasi) karena ini akan memungkinkan kami untuk menjalankan contoh pengujian DB yang diisi dengan data benih yang dapat digunakan untuk menjalankan pernyataan.

Setelah itu, kami membuat objek skema Mongoose dan menggunakannya untuk membuat model, dan kemudian mempelajari bagaimana kami dapat memanggil konstruktor model tersebut untuk membuat instance baru darinya. Tersedia pada instance adalah metode save (antara lain), yang bersifat asinkron, dan yang akan memeriksa apakah struktur objek yang kita lewati sesuai dengan skema, menyelesaikan janji jika ya, dan menolak janji dengan ValidationError jika itu tidak. Jika terjadi resolusi, dokumen baru disimpan ke database dan kami merespons dengan HTTP 200 OK/201 CREATED, jika tidak, kami menangkap kesalahan yang dilemparkan di titik akhir kami, dan mengembalikan Permintaan Buruk HTTP 400 ke klien.

Saat kami melanjutkan Anda membangun titik akhir kami, Anda akan mempelajari lebih lanjut tentang beberapa metode yang tersedia pada model dan contoh model.

Menyelesaikan Titik Akhir Kami

Setelah menyelesaikan POST Endpoint, mari kita tangani GET. Seperti yang saya sebutkan sebelumnya, sintaks :id di dalam rute memungkinkan Express mengetahui bahwa id adalah parameter rute, dapat diakses dari req.params . Anda sudah melihat bahwa ketika Anda mencocokkan beberapa ID untuk param "wildcard" di rute, itu dicetak ke layar dalam contoh awal. Misalnya, jika Anda membuat Permintaan GET ke “/books/test-id-123”, maka req.params.id akan menjadi string test-id-123 karena nama param adalah id dengan memiliki rute sebagai HTTP GET /books/:id .

Jadi, yang perlu kita lakukan adalah mengambil ID itu dari objek req dan memeriksa untuk melihat apakah ada dokumen di database kita yang memiliki ID yang sama — sesuatu yang dibuat sangat mudah oleh Mongoose (dan Native Driver).

 app.get('/books/:id', async (req, res) => { const book = await Book.findById(req.params.id); console.log(book); res.send({ book }); });

Anda dapat melihat bahwa dapat diakses pada model kami adalah fungsi yang dapat kami panggil yang akan menemukan dokumen dengan ID-nya. Di balik layar, Mongoose akan memberikan ID apa pun yang kami berikan ke findById ke jenis bidang _id pada dokumen, atau dalam hal ini, ObjectId . Jika ID yang cocok ditemukan (dan hanya satu yang akan ditemukan karena ObjectId memiliki kemungkinan tabrakan yang sangat rendah), dokumen itu akan ditempatkan di variabel konstanta book kami. Jika tidak, book akan menjadi nol — fakta yang akan kami gunakan dalam waktu dekat.

Untuk saat ini, mari kita restart server (Anda harus me-restart server kecuali Anda menggunakan nodemon ) dan memastikan bahwa kita masih memiliki satu dokumen buku dari sebelumnya di dalam Koleksi Books . Silakan dan salin ID dokumen itu, bagian yang disorot dari gambar di bawah ini:

ObjectID Dokumen Buku
Contoh ObjectID yang akan digunakan untuk Permintaan GET yang akan datang. (Pratinjau besar)

Dan gunakan untuk membuat Permintaan GET ke /books/:id dengan Postman sebagai berikut (perhatikan bahwa data tubuh hanya tersisa dari Permintaan POST saya sebelumnya. Sebenarnya tidak digunakan meskipun faktanya digambarkan pada gambar di bawah) :

GUI Tukang Pos diisi dengan data untuk Permintaan GET.
URL API dan data Postman untuk GET Request. (Pratinjau besar)

Setelah melakukannya, Anda harus mendapatkan kembali dokumen buku dengan ID yang ditentukan di dalam bagian respons Tukang Pos. Perhatikan bahwa sebelumnya, dengan POST Route, yang dirancang untuk "POST" atau "push" sumber daya baru ke server, kami merespons dengan 201 Created — karena sumber daya (atau dokumen) baru telah dibuat. Dalam kasus GET, tidak ada yang baru dibuat — kami hanya meminta sumber daya dengan ID tertentu, sehingga kode status 200 OK adalah yang kami dapatkan, bukan 201 Created.

Seperti yang umum di bidang pengembangan perangkat lunak, kasus tepi harus diperhitungkan — input pengguna secara inheren tidak aman dan salah, dan tugas kita, sebagai pengembang, harus fleksibel terhadap jenis input yang dapat diberikan dan meresponsnya. demikian. Apa yang kami lakukan jika pengguna (atau Pemanggil API) memberikan kami beberapa ID yang tidak dapat dilemparkan ke ObjectID MongoDB, atau ID yang dapat dilemparkan tetapi tidak ada?

Untuk kasus sebelumnya, Mongoose akan melempar CastError — yang dapat dimengerti karena jika kami memberikan ID seperti math-is-fun , maka itu jelas bukan sesuatu yang dapat dilemparkan ke ObjectID, dan casting ke ObjectID secara khusus adalah apa Mongoose melakukan di bawah tenda.

Untuk kasus terakhir, kami dapat dengan mudah memperbaiki masalah melalui Pemeriksaan Null atau Klausul Penjaga. Either way, saya akan mengirim kembali dan HTTP 404 Not Found Response. Saya akan menunjukkan beberapa cara kita bisa melakukan ini, cara yang buruk dan kemudian cara yang lebih baik.

Pertama, kita bisa melakukan hal berikut:

 app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) throw new Error(); return res.send({ book }); } catch (e) { return res.status(404).send({ error: 'Not Found' }); } });

Ini berfungsi dan kita dapat menggunakannya dengan baik. Saya berharap pernyataan await Book.findById() akan melempar CastError Mongoose jika string ID tidak dapat dilemparkan ke ObjectID, menyebabkan blok catch untuk dieksekusi. Jika dapat dilemparkan tetapi ObjectID yang sesuai tidak ada, maka book akan menjadi null dan Pemeriksaan Null akan menimbulkan kesalahan, sekali lagi menembakkan blok catch . Di dalam catch , kami baru saja mengembalikan 404. Ada dua masalah di sini. Pertama, bahkan jika Buku ditemukan tetapi beberapa kesalahan lain yang tidak diketahui terjadi, kami mengirim kembali 404 ketika kami mungkin harus memberi klien generik catch-all 500. Kedua, kami tidak benar-benar membedakan antara apakah ID yang dikirim valid tetapi tidak ada, atau apakah itu hanya ID yang buruk.

Jadi, inilah cara lain:

 const mongoose = require('mongoose'); app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) return res.status(404).send({ error: 'Not Found' }); return res.send({ book }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Hal yang menyenangkan tentang ini adalah kita dapat menangani ketiga kasus 400, 404, dan 500 generik. Perhatikan bahwa setelah Pemeriksaan Null pada book , saya menggunakan kata kunci return pada tanggapan saya. Ini sangat penting karena kami ingin memastikan kami keluar dari pengendali rute di sana.

Beberapa opsi lain mungkin bagi kita untuk memeriksa apakah id pada req.params dapat dilemparkan ke ObjectID secara eksplisit sebagai lawan mengizinkan Mongoose untuk dilemparkan secara implisit dengan mongoose.Types.ObjectId.isValid('id); , tetapi ada kasus tepi dengan string 12-byte yang menyebabkan ini terkadang bekerja secara tidak terduga.

Kami dapat membuat pengulangan tersebut tidak terlalu menyakitkan dengan Boom , perpustakaan Respons HTTP, misalnya, atau kami dapat menggunakan Middleware Penanganan Kesalahan. Kami juga dapat mengubah Kesalahan Mongoose menjadi sesuatu yang lebih mudah dibaca dengan Kait Luwak/Middleware seperti yang dijelaskan di sini. Opsi tambahan adalah mendefinisikan objek kesalahan khusus dan menggunakan Middleware Penanganan Kesalahan Ekspres global, namun, saya akan menyimpannya untuk artikel mendatang di mana kita membahas metode arsitektur yang lebih baik.

Di titik akhir untuk PATCH /books/:id , kami mengharapkan objek pembaruan dilewatkan yang berisi pembaruan untuk buku yang dimaksud. Untuk artikel ini, kami akan mengizinkan semua bidang diperbarui, tetapi di masa mendatang, saya akan menunjukkan bagaimana kami dapat melarang pembaruan bidang tertentu. Selain itu, Anda akan melihat bahwa logika penanganan kesalahan di PATCH Endpoint kami akan sama dengan GET Endpoint kami. Itu indikasi bahwa kami melanggar Prinsip KERING, tetapi sekali lagi, kami akan membahasnya nanti.

Saya berharap bahwa semua pembaruan tersedia pada objek updates req.body (artinya klien akan mengirimkan JSON yang berisi objek updates ) dan akan menggunakan fungsi Book.findByAndUpdate dengan tanda khusus untuk melakukan pembaruan.

 app.patch('/books/:id', async (req, res) => { const { id } = req.params; const { updates } = req.body; try { const updatedBook = await Book.findByIdAndUpdate(id, updates, { runValidators: true, new: true }); if (!updatedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: updatedBook }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Perhatikan beberapa hal di sini. Kami pertama-tama merusak id dari req.params dan updates dari req.body .

Tersedia pada model Book adalah fungsi dengan nama findByIdAndUpdate yang mengambil ID dokumen yang dimaksud, pembaruan untuk dilakukan, dan objek opsi opsional. Biasanya, Mongoose tidak akan melakukan validasi ulang untuk operasi pembaruan, jadi runValidators: true yang kami berikan saat objek options memaksanya untuk melakukannya. Selanjutnya, pada Mongoose 4, Model.findByIdAndUpdate tidak lagi mengembalikan dokumen yang dimodifikasi tetapi mengembalikan dokumen asli sebagai gantinya. Bendera new: true (yang salah secara default) menimpa perilaku itu.

Akhirnya, kita dapat membangun titik akhir DELETE kita, yang sangat mirip dengan yang lainnya:

 app.delete('/books/:id', async (req, res) => { try { const deletedBook = await Book.findByIdAndDelete(req.params.id); if (!deletedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: deletedBook }); } catch (e) { if (e instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { return res.status(500).send({ error: 'Internal Error' }); } } });

Dengan itu, API primitif kami selesai dan Anda dapat mengujinya dengan membuat Permintaan HTTP ke semua titik akhir.

Penafian Singkat Tentang Arsitektur Dan Bagaimana Kami Akan Memperbaikinya

Dari sudut pandang arsitektur, kode yang kami miliki di sini cukup buruk, berantakan, tidak KERING, tidak SOLID, bahkan Anda mungkin menyebutnya menjijikkan. Apa yang disebut "Penangan Rute" ini melakukan lebih dari sekadar "menyerahkan rute" — mereka berinteraksi langsung dengan database kami. Itu berarti sama sekali tidak ada abstraksi.

Mari kita hadapi itu, sebagian besar aplikasi tidak akan pernah sekecil ini atau Anda mungkin bisa lolos dengan arsitektur tanpa server dengan Firebase Database. Mungkin, seperti yang akan kita lihat nanti, pengguna menginginkan kemampuan untuk mengunggah avatar, kutipan, dan cuplikan dari buku mereka, dll. Mungkin kami ingin menambahkan fitur obrolan langsung antara pengguna dengan WebSockets, dan mari kita bahkan mengatakan bahwa kita 'll membuka aplikasi kami untuk memungkinkan pengguna meminjam buku satu sama lain dengan sedikit biaya — di mana kami perlu mempertimbangkan Integrasi Pembayaran dengan Stripe API dan pengiriman logistik dengan Shippo API.

Misalkan kita melanjutkan dengan arsitektur kita saat ini dan menambahkan semua fungsi ini. Pengendali rute ini, juga dikenal sebagai Tindakan Pengendali, akan menjadi sangat, sangat besar dengan kompleksitas siklomatik yang tinggi . Gaya pengkodean seperti itu mungkin cocok untuk kita di masa-masa awal, tetapi bagaimana jika kita memutuskan bahwa data kita adalah referensial dan dengan demikian PostgreSQL adalah pilihan database yang lebih baik daripada MongoDB? Kita sekarang harus memfaktorkan ulang seluruh aplikasi kita, menghapus Mongoose, mengubah Controller kita, dll., yang semuanya dapat menyebabkan potensi bug di logika bisnis lainnya. Contoh lainnya adalah memutuskan bahwa AWS S3 terlalu mahal dan kami ingin bermigrasi ke GCP. Sekali lagi, ini membutuhkan refactor seluruh aplikasi.

Meskipun ada banyak pendapat seputar arsitektur, mulai dari Desain Berbasis Domain, Segregasi Tanggung Jawab Kueri Perintah, dan Sumber Acara, hingga Pengembangan Berbasis Uji, SOLID, Arsitektur Berlapis, Arsitektur Bawang, dan banyak lagi, kami akan fokus pada penerapan Arsitektur Berlapis sederhana di artikel mendatang, yang terdiri dari Controllers, Services, dan Repositories, dan menggunakan Design Patterns seperti Composition, Adapters/Wrappers, dan Inversion of Control via Dependency Injection. Meskipun, sampai batas tertentu, ini dapat dilakukan dengan JavaScript, kita akan melihat opsi TypeScript untuk mencapai arsitektur ini juga, yang memungkinkan kita untuk menggunakan paradigma pemrograman fungsional seperti Either Monads selain konsep OOP seperti Generics.

Untuk saat ini, ada dua perubahan kecil yang bisa kita lakukan. Karena logika penanganan kesalahan kami sangat mirip di blok catch semua titik akhir, kami dapat mengekstraknya ke fungsi Middleware Penanganan Kesalahan Ekspres khusus di bagian paling akhir tumpukan.

Membersihkan Arsitektur Kami

Saat ini, kami mengulangi sejumlah besar logika penanganan kesalahan di semua titik akhir kami. Sebagai gantinya, kita dapat membangun fungsi Express Error Handling Middleware, yang merupakan Fungsi Express Middleware yang dipanggil dengan kesalahan, objek req dan res, dan fungsi berikutnya.

Untuk saat ini, mari kita bangun fungsi middleware itu. Yang akan saya lakukan adalah mengulangi logika penanganan kesalahan yang sama seperti yang biasa kita lakukan:

 app.use((err, req, res, next) => { if (err instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'Validation Error' }); } else if (err instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { console.log(err); // Unexpected, so worth logging. return res.status(500).send({ error: 'Internal error' }); } });

Ini tampaknya tidak berfungsi dengan Kesalahan Mongoose, tetapi secara umum, daripada menggunakan if/else if/else untuk menentukan contoh kesalahan, Anda dapat mengganti konstruktor kesalahan. Aku akan meninggalkan apa yang kita miliki, namun.

Dalam penangan titik akhir/rute sinkron , jika Anda membuat kesalahan, Express akan menangkapnya dan memprosesnya tanpa perlu kerja ekstra dari Anda. Sayangnya, tidak demikian bagi kami. Kita berurusan dengan kode asinkron . Untuk mendelegasikan penanganan kesalahan ke Express dengan penangan rute async, kami banyak menangkap kesalahan itu sendiri dan meneruskannya ke next() .

Jadi, saya hanya akan mengizinkan argumen next menjadi argumen ketiga ke titik akhir, dan saya akan menghapus logika penanganan kesalahan di blok catch demi meneruskan instance kesalahan ke next , seperti:

 app.post('/books', async (req, res, next) => { try { const book = new Book(req.body.book); await book.save(); return res.send({ book }); } catch (e) { next(e) } });

Jika Anda melakukan ini ke semua penangan rute, Anda akan mendapatkan kode berikut:

 const express = require('express'); const mongoose = require('mongoose'); // Database connection and model. require('./db/mongoose.js')(); const Book = require('./models/book.js'); // This creates our Express App. const app = express(); // Define middleware. app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Listening on port 3000 (arbitrary). // Not a TCP or UDP well-known port. // Does not require superuser privileges. const PORT = 3000; // We will build our API here. // HTTP POST /books app.post('/books', async (req, res, next) => { try { const book = new Book(req.body.book); await book.save(); return res.status(201).send({ book }); } catch (e) { next(e) } }); // HTTP GET /books/:id app.get('/books/:id', async (req, res) => { try { const book = await Book.findById(req.params.id); if (!book) return res.status(404).send({ error: 'Not Found' }); return res.send({ book }); } catch (e) { next(e); } }); // HTTP PATCH /books/:id app.patch('/books/:id', async (req, res, next) => { const { id } = req.params; const { updates } = req.body; try { const updatedBook = await Book.findByIdAndUpdate(id, updates, { runValidators: true, new: true }); if (!updatedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: updatedBook }); } catch (e) { next(e); } }); // HTTP DELETE /books/:id app.delete('/books/:id', async (req, res, next) => { try { const deletedBook = await Book.findByIdAndDelete(req.params.id); if (!deletedBook) return res.status(404).send({ error: 'Not Found' }); return res.send({ book: deletedBook }); } catch (e) { next(e); } }); // Notice - bottom of stack. app.use((err, req, res, next) => { if (err instanceof mongoose.Error.ValidationError) { return res.status(400).send({ error: 'Validation Error' }); } else if (err instanceof mongoose.Error.CastError) { return res.status(400).send({ error: 'Not a valid ID' }); } else { console.log(err); // Unexpected, so worth logging. return res.status(500).send({ error: 'Internal error' }); } }); // Binding our application to port 3000. app.listen(PORT, () => console.log(`Server is up on port ${PORT}.`));

Lebih jauh lagi, ada baiknya memisahkan middleware penanganan kesalahan kita ke dalam file lain, tapi itu sepele, dan kita akan melihatnya di artikel mendatang dalam seri ini. Selain itu, kita dapat menggunakan modul NPM bernama express-async-errors untuk mengizinkan kita tidak perlu memanggil berikutnya di blok catch, tetapi sekali lagi, saya mencoba menunjukkan kepada Anda bagaimana semuanya dilakukan secara resmi.

Sepatah Kata Tentang CORS Dan Kebijakan Asal Yang Sama

Misalkan situs web Anda dilayani dari domain myWebsite.com tetapi server Anda berada di myOtherDomain.com/api . CORS adalah singkatan dari Cross-Origin Resource Sharing dan merupakan mekanisme di mana permintaan lintas domain dapat dilakukan. Dalam kasus di atas, karena server dan kode JS front-end berada di domain yang berbeda, Anda akan membuat permintaan di dua asal yang berbeda, yang biasanya dibatasi oleh browser untuk alasan keamanan, dan dikurangi dengan menyediakan header HTTP tertentu.

Kebijakan Asal yang Sama adalah yang melakukan pembatasan yang disebutkan di atas — browser web hanya akan mengizinkan persyaratan untuk dibuat di asal yang sama.

Kami akan menyentuh CORS dan SOP nanti ketika kami membangun front-end yang dibundel Webpack untuk API Buku kami dengan React.

Kesimpulan Dan Apa Selanjutnya

Kami telah membahas banyak hal dalam artikel ini. Mungkin tidak semuanya praktis, tapi semoga membuat Anda lebih nyaman bekerja dengan fitur JavaScript Express dan ES6. Jika Anda baru mengenal pemrograman dan Node adalah jalur pertama yang Anda mulai, semoga referensi untuk mengetik bahasa statis seperti Java, C++, dan C# membantu menyoroti beberapa perbedaan antara JavaScript dan rekan statisnya.

Next time, we'll finish building out our Book API by making some fixes to our current setup with regards to the Book Routes, as well as adding in User Authentication so that users can own books. We'll do all of this with a similar architecture to what I described here and with MongoDB for data persistence. Finally, we'll permit users to upload avatar images to AWS S3 via Buffers.

In the article thereafter, we'll be rebuilding our application from the ground up in TypeScript, still with Express. We'll also move to PostgreSQL with Knex instead of MongoDB with Mongoose as to depict better architectural practices. Finally, we'll update our avatar image uploading process to use Node Streams (we'll discuss Writable, Readable, Duplex, and Transform Streams). Along the way, we'll cover a great amount of design and architectural patterns and functional paradigms, including:

  • Controllers/Controller Actions
  • Jasa
  • Repositori
  • Data Mapping
  • The Adapter Pattern
  • The Factory Pattern
  • The Delegation Pattern
  • OOP Principles and Composition vs Inheritance
  • Inversion of Control via Dependency Injection
  • SOLID Principles
  • Coding against interfaces
  • Data Transfer Objects
  • Domain Models and Domain Entities
  • Either Monads
  • Validation
  • Decorators
  • Logging and Logging Levels
  • Unit Tests, Integration Tests (E2E), and Mutation Tests
  • The Structured Query Language
  • Hubungan
  • HTTP/Express Security Best Practices
  • Node Best Practices
  • OWASP Security Best Practices
  • Dan banyak lagi.

Using that new architecture, in the article after that, we'll write Unit, Integration, and Mutation tests, aiming for close to 100 percent testing coverage, and we'll finally discuss setting up a remote CI/CD pipeline with CircleCI, as well as Message Busses, Job/Task Scheduling, and load balancing/reverse proxying.

Hopefully, this article has been helpful, and if you have any queries or concerns, let me know in the comments below.