Bir Express ve ES6+ JavaScript Yığınıyla Başlarken

Yayınlanan: 2022-03-10
Hızlı özet ↬ Express ile arka uç web uygulaması geliştirme sürecine temel bir giriş — en yeni ES6+ JavaScript özelliklerini, Fabrika Tasarım Kalıbını, MongoDB CRUD İşlemlerini, sunucuları ve bağlantı noktalarını ve TypeScript projeleri için kurumsal n-katmanlı mimari kalıplarla geleceği tartışıyor.

Bu makale, Node.js, ES6+ JavaScript, Geri Çağırma İşlevleri, Ok İşlevleri, API'ler, HTTP Protokolü, JSON, MongoDB ve daha fazla.

Bu makalede, kullanıcı kitap listesi bilgilerini depolamak için bir MongoDB Veritabanının nasıl uygulanacağını ve konuşlandırılacağını öğrenerek, bu veritabanını ortaya çıkarmak için Node.js ve Express Web Uygulaması çerçevesiyle bir API oluşturacak şekilde bir öncekinde edindiğimiz becerilerin üzerine inşa edeceğiz. ve üzerinde CRUD İşlemleri gerçekleştirin ve daha fazlasını yapın. Yol boyunca, ES6 Object Destructuring, ES6 Object Shorthand, Async/Await sözdizimi, Spread Operator'u tartışacağız ve CORS'a, Same Origin Policy'ye ve daha fazlasına kısaca göz atacağız.

Daha sonraki bir makalede, üç katmanlı mimariyi kullanarak ve Dependency Injection aracılığıyla Kontrolün Tersine Çevirmesini sağlayarak, endişeleri ayırmak için kod tabanımızı yeniden gözden geçireceğiz, JSON Web Token ve Firebase Kimlik Doğrulaması tabanlı güvenlik ve erişim kontrolü gerçekleştireceğiz, nasıl güvenli bir şekilde yapılacağını öğreneceğiz. parolaları depolayın ve kullanıcı avatarlarını Node.js Tamponları ve Akışları ile depolamak için AWS Simple Storage Service'i kullanın - tüm bunları yaparken de veri kalıcılığı için PostgreSQL'i kullanın. Bu arada, Klasik OOP kavramlarını (Polimorfizm, Kalıtım, Kompozisyon vb.) incelemek ve hatta Fabrikalar ve Adaptörler gibi tasarım kalıplarını incelemek için kod tabanımızı TypeScript'te sıfırdan yeniden yazacağız.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Uyarı kelimesi

Bugün Node.js'yi tartışan makalelerin çoğunda bir sorun var. Hepsi değil, çoğu, Express Routing'in nasıl kurulacağını, Mongoose'u nasıl entegre edeceğini ve belki de JSON Web Token Authentication'ı nasıl kullanacağını anlatmaktan öteye gitmez. Sorun şu ki, mimariden veya en iyi güvenlik uygulamalarından veya temiz kodlama ilkelerinden veya ACID Uyumluluğu, İlişkisel Veritabanları, Beşinci Normal Form, CAP Teoremi veya İşlemler hakkında konuşmazlar. Ya tüm bunların geldiğini bildiğiniz ya da yukarıda bahsedilen bilgiyi garanti edecek kadar büyük veya popüler projeler inşa etmeyeceğiniz varsayılır.

Birkaç farklı Düğüm geliştiricisi türü var gibi görünüyor - diğerleri arasında, bazıları genel olarak programlama konusunda yenidir ve diğerleri, C# ve .NET Framework veya Java Spring Framework ile uzun bir kurumsal geliştirme geçmişinden gelmektedir. Makalelerin çoğu eski gruba hitap ediyor.

Bu makalede, çok fazla makalenin yaptığını söylediğim şeyi tam olarak yapacağım, ancak bir sonraki makalede, Dependency Injection, Three- gibi ilkeleri açıklamama izin vererek kod tabanımızı tamamen yeniden düzenleyeceğiz. Katman Mimarisi (Denetleyici/Hizmet/Depo), Veri Eşleme ve Aktif Kayıt, tasarım desenleri, birim, entegrasyon ve mutasyon testi, SOLID İlkeleri, İş Birimi, arayüzlere karşı kodlama, HSTS, CSRF, NoSQL ve SQL Enjeksiyon gibi en iyi güvenlik uygulamaları Önleme ve benzeri. Ayrıca, bir ORM yerine basit sorgu oluşturucu Knex'i kullanarak MongoDB'den PostgreSQL'e geçeceğiz - bu, kendi veri erişim altyapımızı oluşturmamıza ve farklı ilişki türleri (Tek- Bire, Çoktan Çoka vb.) ve daha fazlası. O halde bu makale yeni başlayanlara hitap etmelidir, ancak sonraki birkaç makale, mimarilerini geliştirmek isteyen daha orta düzey geliştiricilere hitap etmelidir.

Bunda, yalnızca kalıcı kitap verileri hakkında endişeleneceğiz. Kullanıcı kimlik doğrulaması, parola karma, mimari veya bunun gibi karmaşık herhangi bir şeyle ilgilenmeyeceğiz. Bunların hepsi sonraki ve gelecekteki makalelerde gelecek. Şimdilik ve çok temel olarak, bir müşterinin web sunucumuzla HTTP Protokolü aracılığıyla iletişim kurmasına ve kitap bilgilerini bir veritabanına kaydetmesine izin verecek bir yöntem oluşturacağız.

Not : Bunu kasıtlı olarak son derece basit tuttum ve belki de burada o kadar da pratik değil çünkü bu makale kendi başına son derece uzun, çünkü ek konuları tartışmak için sapma özgürlüğümü kullandım. Bu nedenle, bu seride API'nin kalitesini ve karmaşıklığını aşamalı olarak artıracağız, ancak bunu Express'e ilk girişlerinizden biri olarak düşündüğüm için, kasıtlı olarak işleri son derece basit tutuyorum.

  1. ES6 Nesne Yıkımı
  2. ES6 Nesne Steno
  3. ES6 Yayılma Operatörü (...)
  4. geliyor...

ES6 Nesne Yıkımı

ES6 Object Destructuring veya Destructuring Assignment Syntax, dizilerden veya nesnelerden değerleri kendi değişkenlerine çıkarmak veya açmak için kullanılan bir yöntemdir. Nesne özellikleriyle başlayacağız ve ardından dizi öğelerini tartışacağız.

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

Böyle bir işlem oldukça ilkeldir, ancak her yerde person.something atıfta bulunmaya devam etmemiz gerektiği düşünülürse bu biraz güç olabilir. Kodumuzda bunu yapmamız gereken 10 başka yer olduğunu varsayalım - oldukça hızlı bir şekilde oldukça zorlaşacaktı. Bir kısalık yöntemi, bu değerleri kendi değişkenlerine atamak olacaktır.

 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);

Belki bu mantıklı görünüyor, ama ya person nesnesinde yuvalanmış 10 başka özelliğimiz olsaydı? Bu, yalnızca değişkenlere değer atamak için birçok gereksiz satır olurdu - bu noktada tehlikede oluruz çünkü nesne özellikleri mutasyona uğrarsa, değişkenlerimiz bu değişikliği yansıtmaz (unutmayın, yalnızca nesneye yapılan referanslar const atama ile değişmezdir, nesnenin özellikleri değil), yani temelde artık “durumu” (ve bu kelimeyi gevşek bir şekilde kullanıyorum) senkronize halde tutamayız. Burada referansa göre geçiş ve değere göre geçiş devreye girebilir, ancak bu bölümün kapsamından çok uzaklaşmak istemiyorum.

ES6 Object Destructing temel olarak şunu yapmamızı sağlar:

 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);

Yeni bir nesne/nesne değişmezi oluşturmuyoruz, orijinal nesneden name ve occupation özelliklerini açıyor ve aynı adlı kendi değişkenlerine koyuyoruz. Kullandığımız adlar, çıkarmak istediğimiz özellik adlarıyla eşleşmelidir.

Yine, sözdizimi const { a, b } = someObject; özellikle, someObject içinde bazı a özelliklerinin ve bazı b özelliklerinin var olmasını beklediğimizi (örneğin, someObject { a: 'dataA', b: 'dataB' } ) ve değerler ne olursa olsun yerleştirmek istediğimizi söylüyor. aynı ada sahip const değişkenleri içindeki bu anahtarların/özelliklerin. Bu nedenle yukarıdaki sözdizimi bize const a = someObject.a ve const b = someObject.b olmak üzere iki değişken sağlar.

Bunun anlamı, Nesne Yıkımının iki yönü olduğudur. "Şablon" tarafı ve "Kaynak" tarafı, burada const { a, b } tarafı (sol taraf) şablon ve someObject tarafı (sağ taraf) kaynak taraftır - bu mantıklıdır — sol tarafta "kaynak" tarafındaki verileri yansıtan bir yapı veya "şablon" tanımlıyoruz.

Yine, sadece bunu açıklığa kavuşturmak için, işte birkaç örnek:

 // ----- 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

İç içe özellikler durumunda, aynı yapıyı yok etme atamanızda yansıtın:

 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

Gördüğünüz gibi, çıkarmaya karar verdiğiniz özellikler isteğe bağlıdır ve iç içe geçmiş özellikleri açmak için, orijinal nesnenin (kaynak) yapısını yok etme sözdiziminizin şablon tarafında yansıtmanız yeterlidir. Orijinal nesnede olmayan bir özelliği yok etmeye çalışırsanız, bu değer tanımsız olacaktır.

Ek olarak, aşağıdaki sözdizimini kullanarak bir değişkeni önceden bildirmeden - bildirimsiz atama - yok edebiliriz:

 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

Yanlışlıkla bir önceki satırda bir işlevle (böyle bir işlev varsa) bir IIFE (Hemen Çağrılan İşlev İfadesi) oluşturmadığımızdan emin olmak için ifadenin önüne noktalı virgül koyarız ve atama ifadesinin etrafındaki parantezler aşağıdakiler için gereklidir: JavaScript'in sol (şablon) tarafınızı bir blok olarak işlemesini durdurun.

İşlev bağımsız değişkenleri içinde çok yaygın bir yıkım kullanım durumu vardır:

 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);

Gördüğünüz gibi, fonksiyonun içinde artık alıştığımız normal yok etme sözdizimini şu şekilde kullanabilirdik:

 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);

Ancak söz konusu sözdizimini fonksiyon imzasının içine yerleştirmek, otomatik olarak tahribat yapar ve bize bir satır kazandırır.

Bunun gerçek dünyadaki bir kullanım durumu, props için React Functional Components içindedir:

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

Aksine:

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

Her iki durumda da, özelliklere varsayılan değerler de ayarlayabiliriz:

 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

Görüldüğü gibi, yok edildiğinde name mevcut olmaması durumunda, ona varsayılan bir değer sağlıyoruz. Bunu önceki sözdizimi ile de yapabiliriz:

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

Diziler de yapılandırılabilir:

 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

Dizi yıkımının pratik bir nedeni, React Hooks ile ortaya çıkar. (Ve başka birçok neden var, sadece örnek olarak React kullanıyorum).

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

useState dışa aktarmadan bozulduğuna ve dizi işlevlerinin/değerlerinin useState kancasından kaldırıldığına dikkat edin. Yine, yukarıdakiler mantıklı gelmiyorsa endişelenmeyin - React'i anlamanız gerekir - ve ben sadece örnek olarak kullanıyorum.

ES6 Object Destructuring hakkında daha fazla şey olsa da, burada bir konuyu daha ele alacağım: Destructuring Yeniden Adlandırma, kapsam çarpışmalarını veya değişken gölgeleri vb. önlemek için yararlıdır. Diyelim ki name adlı bir özelliği, person adlı bir nesneden yok etmek istiyoruz, ancak kapsamda name göre bir değişken zaten var. İki nokta üst üste ile anında yeniden adlandırabiliriz:

 // 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.

Son olarak, yeniden adlandırma ile de varsayılan değerleri ayarlayabiliriz:

 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

Gördüğünüz gibi bu durumda name from person ( person.name ) personName olarak yeniden adlandırılacak ve mevcut değilse Anonymous varsayılan değerine ayarlanacaktır.

Ve elbette, aynısı fonksiyon imzalarında da yapılabilir:

 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

ES6 Nesne Steno

Aşağıdaki fabrikaya sahip olduğunuzu varsayalım: (fabrikaları daha sonra ele alacağız)

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

Aşağıdaki gibi bir person nesnesi oluşturmak için bu fabrika kullanılabilir. Ayrıca, fabrikanın, Ok İşlevinin parantezlerinin etrafındaki parantezlerde açıkça görülen bir nesneyi dolaylı olarak döndürdüğünü unutmayın.

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

ES5 Nesne Değişmez Sözdiziminden zaten bildiğimiz şey bu. Ancak fabrika işlevinde, her özelliğin değerinin, özellik tanımlayıcısının (anahtarının) kendisiyle aynı ad olduğuna dikkat edin. Yani — location: location veya name: name . Bunun JS geliştiricilerinde oldukça yaygın bir olay olduğu ortaya çıktı.

ES6'nın kısayol sözdizimi ile fabrikayı aşağıdaki gibi yeniden yazarak aynı sonucu elde edebiliriz:

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

Çıktı üretmek:

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

Bu stenografiyi yalnızca, oluşturmak istediğimiz nesne değişkenlere dayalı olarak dinamik olarak oluşturulduğunda , değişken adlarının, değişkenlerin atanmasını istediğimiz özelliklerin adlarıyla aynı olduğu durumlarda kullanabileceğimizi anlamak önemlidir.

Bu aynı sözdizimi, nesne değerleriyle çalışır:

 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);

Çıktı üretmek:

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

Son bir örnek olarak, bu, nesne değişmezleriyle de çalışır:

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

ES6 Yayılma Operatörü (…)

Yayılma Operatörü, bazılarını burada tartışacağımız çeşitli şeyler yapmamıza izin verir.

İlk olarak, özellikleri bir nesneden başka bir nesneye yayabiliriz:

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

Bu, myObjOne üzerindeki tüm özellikleri myObjOne üzerine, myObjTwo artık { a: 'a', b: 'b' } myObjTwo şekilde yerleştirme etkisine sahiptir. Önceki özellikleri geçersiz kılmak için bu yöntemi kullanabiliriz. Bir kullanıcının hesabını güncellemek istediğini varsayalım:

 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' } */

Aynısı dizilerle de gerçekleştirilebilir:

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

Burada dizileri yeni bir diziye yayarak her iki kümenin (dizilerin) bir birleşimini oluşturduğumuza dikkat edin.

Dinlenme/Yayma Operatörü hakkında çok daha fazlası var, ancak bu makalenin kapsamı dışında. Örneğin, bir işleve birden çok argüman elde etmek için kullanılabilir. Daha fazlasını öğrenmek istiyorsanız, MDN Belgelerini buradan görüntüleyin.

ES6 Zaman uyumsuz/Bekliyor

Async/Await, söz zincirlemenin acısını hafifletmek için bir sözdizimidir.

await ayrılmış anahtar sözcüğü, bir sözün yerine getirilmesini "beklemenize" izin verir, ancak yalnızca async anahtar sözcüğüyle işaretlenmiş işlevlerde kullanılabilir. Bir söz veren bir işlevim olduğunu varsayalım. Yeni bir zaman async işlevinde, await ve .then kullanmak yerine bu sözün sonucunu .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();

Burada dikkat edilmesi gereken birkaç şey var. Bir await işlevinde async kullandığımızda, sol taraftaki değişkene yalnızca çözümlenen değer gider. Eğer fonksiyon reddederse, birazdan göreceğimiz gibi, bu yakalamamız gereken bir hatadır. Ek olarak, zaman async olarak işaretlenen herhangi bir işlev, varsayılan olarak bir söz verir.

Biri öncekinden gelen yanıtla birlikte iki API çağrısı yapmam gerektiğini varsayalım. Sözler ve söz zincirini kullanarak bunu şu şekilde yapabilirsiniz:

 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 */

Burada olan şey, ilk kez makeAPICall /whatever 'a geçmemizdir. Söz bu değerle çözülür. Daha sonra makeAPICall tekrar çağırırız, ona /whatever second call , günlüğe kaydedilir ve tekrar söz bu yeni değerle çözülür. Son olarak, sözün çözüldüğü bu yeni değeri /whatever second call alırız ve sonunda log on ekleyerek son logged kendimiz kaydederiz. Bu mantıklı gelmiyorsa, söz zincirine bakmalısınız.

async / await kullanarak, aşağıdakileri yeniden düzenleyebiliriz:

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

İşte olacaklar. await için yapılan ilk çağrının verdiği söz çözülene kadar tüm işlev ilk makeAPICall , çözümleme üzerine çözümlenen değer resultOne içine yerleştirilecektir. Bu olduğunda, işlev ikinci await ifadesine geçecek ve söz verme süresi boyunca yine orada duraklayacaktır. Söz çözüldüğünde, çözüm sonucu resultTwo içine yerleştirilecektir. İşlev yürütme fikri kulağa engelleyici geliyorsa, korkmayın, hala eşzamansızdır ve nedenini birazdan tartışacağım.

Bu sadece “mutlu” yolu gösterir. Sözlerden birinin reddedilmesi durumunda, bunu dene/yakala ile yakalayabiliriz, çünkü söz reddedilirse, bir hata atılır - bu, söz hangi hatayla reddedilirse o olacaktır.

 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) } };

Daha önce söylediğim gibi, async olarak bildirilen herhangi bir işlev bir söz verecektir. Bu nedenle, başka bir işlevden bir zaman uyumsuz işlevi çağırmak istiyorsanız, normal vaatleri kullanabilir veya çağrı işlevini async olarak bildirirseniz await . Ancak, üst düzey koddan bir zaman async işlevi çağırmak ve sonucunu beklemek istiyorsanız, .then ve .catch kullanmanız gerekir.

Örneğin:

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

Veya Hemen Çağrılan İşlev İfadesini (IIFE) kullanabilirsiniz:

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

Bir zaman async işlevinde await kullandığınızda, söz verilene kadar işlevin yürütülmesi bu wait ifadesinde duracaktır. Bununla birlikte, diğer tüm işlevler yürütmeye devam etmekte serbesttir, bu nedenle fazladan CPU kaynağı tahsis edilmez ve iş parçacığı hiç bloke edilmez. Bunu tekrar söyleyeceğim - o belirli zamanda söz konusu belirli işlevdeki işlemler, söz verilene kadar duracaktır, ancak diğer tüm işlevler ateşlenmekte serbesttir. Bir HTTP Web Sunucusu düşünün - istek bazında, istekler yapılırken tüm işlevler tüm kullanıcılar için aynı anda tetiklenmekte serbesttir, yalnızca zaman uyumsuz/bekleme sözdizimi, bir işlemin eşzamanlı olduğu ve yapmak için engellendiği yanılsamasını sağlayacaktır. çalışmak daha kolay vaat ediyor, ancak yine her şey güzel ve zaman uyumsuz kalacak.

async / await için yapılacakların hepsi bu değil, ancak temel ilkeleri kavramanıza yardımcı olacaktır.

Klasik OOP Fabrikaları

Artık JavaScript dünyasından çıkıp Java dünyasına gireceğiz. Bir nesnenin (bu durumda, bir sınıf örneği - yine Java) oluşturma sürecinin oldukça karmaşık olduğu veya bir dizi parametreye dayalı olarak farklı nesnelerin üretilmesini istediğimiz bir zaman gelebilir. Bir örnek, farklı hata nesneleri oluşturan bir işlev olabilir. Fabrika, Nesne Yönelimli Programlamada yaygın bir tasarım modelidir ve temel olarak nesneler yaratan bir işlevdir. Bunu keşfetmek için JavaScript'ten Java dünyasına geçelim. Bu, Klasik OOP'den (yani prototip değil), statik olarak yazılmış bir dil arka planından gelen geliştiriciler için anlamlı olacaktır. Böyle bir geliştirici değilseniz, bu bölümü atlamaktan çekinmeyin. Bu küçük bir sapmadır ve bu nedenle, burayı takip etmek JavaScript akışınızı kesintiye uğratıyorsa, lütfen bu bölümü atlayın.

Ortak bir yaratılış kalıbı olan Fabrika Kalıbı, söz konusu yaratmayı gerçekleştirmek için gerekli iş mantığını açığa çıkarmadan nesneler yaratmamıza izin verir.

İlkel şekilleri n-boyutlarda görselleştirmemize izin veren bir program yazdığımızı varsayalım. Örneğin bir küp sağlarsak, 2B küp (kare), 3B küp (küp) ve 4B küp (Tesseract veya Hiperküp) görürüz. Java'da bunun, önemsiz bir şekilde ve asıl çizim bölümünü engellemenin nasıl yapılabileceği aşağıda açıklanmıştır.

 // 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. } }

Gördüğünüz gibi, bir şekil çizme yöntemini belirten bir arayüz tanımlıyoruz. Farklı sınıfların arayüzü uygulamasını sağlayarak, tüm şekillerin çizilebileceğini garanti edebiliriz (çünkü hepsinin arayüz tanımına göre geçersiz kılınabilir bir draw yöntemine sahip olması gerekir). Bu şeklin, içinde görüntülendiği boyutlara bağlı olarak farklı şekilde çizildiğini göz önünde bulundurarak, arabirimi uygulayan yardımcı sınıfları, n-boyutlu oluşturma simülasyonunun GPU yoğun çalışmasını gerçekleştirecek şekilde tanımlarız. ShapeFactory , doğru sınıfı başlatma işini yapar - createShape yöntemi bir fabrikadır ve yukarıdaki tanım gibi, bir sınıfın nesnesini döndüren bir yöntemdir. createShape dönüş türü IShape arabirimidir, çünkü IShape arabirimi tüm şekillerin temel türüdür (çünkü bir draw yöntemine sahiptirler).

Bu Java örneği oldukça önemsizdir, ancak bir nesne oluşturma kurulumunun o kadar basit olmayabileceği daha büyük uygulamalarda ne kadar yararlı olduğunu kolayca görebilirsiniz. Bunun bir örneği bir video oyunu olabilir. Kullanıcının farklı düşmanlardan hayatta kalması gerektiğini varsayalım. Soyut sınıflar ve arayüzler, tüm düşmanlar için mevcut olan temel işlevleri (ve geçersiz kılınabilecek yöntemleri) tanımlamak için kullanılabilir, belki delegasyon deseni (Dörtlü Çete'nin önerdiği gibi kalıtım yerine kompozisyonu tercih edin, böylece genişletmeye kilitlenmeyin) tek temel sınıf ve test/alay/DI'yi kolaylaştırmak için). Farklı şekillerde somutlaştırılan düşman nesneleri için, arabirim, genel arabirim türüne güvenerek fabrikada nesne oluşturmaya izin verir. Düşman dinamik olarak yaratılmışsa, bu çok alakalı olurdu.

Başka bir örnek, bir oluşturucu işlevidir. Bir sınıf temsilcisinin bir arabirimi onurlandıran diğer sınıflara çalışmasını sağlamak için Temsilci Modelini kullandığımızı varsayalım. Kendi örneğini oluşturması için sınıfa statik bir build yöntemi yerleştirebiliriz (Bağımlılık Enjeksiyon Konteyneri/Çerçevesi kullanmadığınızı varsayarak). Her ayarlayıcıyı aramak yerine şunu yapabilirsiniz:

 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()); } }

Aşina değilseniz, Delegasyon Modelini sonraki bir makalede açıklayacağım - temel olarak, Kompozisyon aracılığıyla ve nesne modelleme açısından, “is-a” yerine “vardır” ilişkisi yaratır Mirasla alacağınız gibi ilişki. Bir Mammal sınıfınız ve bir Dog sınıfınız varsa ve Dog Mammal genişletiyorsa, o zaman Dog is-a Mammal . Oysa, bir Bark sınıfınız varsa ve Bark örneklerini Dog yapıcısına ilettiyseniz, o zaman Dog bir Bark vardır. Tahmin edebileceğiniz gibi, bu özellikle birim testini kolaylaştırır, çünkü sahte, test ortamında arayüz sözleşmesini onurlandırdığı sürece, alaylar enjekte edebilir ve sahte hakkında gerçekleri iddia edebilirsiniz.

Yukarıdaki static "inşa" fabrika yöntemi, yalnızca yeni bir User nesnesi oluşturur ve somut bir MessageService . Bunun yukarıdaki tanımdan nasıl çıktığına dikkat edin - bir sınıfın nesnesini oluşturmak için iş mantığını açığa vurmamak veya bu durumda, mesajlaşma servisinin oluşturulmasını fabrikayı arayan kişiye ifşa etmemek.

Yine, bu, gerçek dünyada işleri nasıl yapacağınız anlamına gelmez, ancak bir fabrika işlevi/yöntemi fikrini oldukça iyi sunar. Örneğin, bunun yerine bir Dependency Injection konteyneri kullanabiliriz. Şimdi JavaScript'e dönelim.

Ekspres ile Başlamak

Express, birinin HTTP Web Sunucusu oluşturmasına izin veren Düğüm için bir Web Uygulama Çerçevesidir (bir NPM Modülü aracılığıyla sağlanır). Express'in bunu yapmak için tek çerçeve olmadığını (Koa, Fastify vb. vardır) ve önceki makalede görüldüğü gibi Node'un Express olmadan bağımsız bir varlık olarak çalışabileceğini belirtmek önemlidir. (Express, yalnızca Node için tasarlanmış bir modüldür - Express, Web Sunucuları için popüler olmasına rağmen, Node onsuz birçok şeyi yapabilir).

Yine çok önemli bir ayrım yapayım. Node/JavaScript ve Express arasında bir ikilik var . JavaScript'i çalıştırdığınız çalışma zamanı/ortam olan düğüm, React Native uygulamaları, masaüstü uygulamaları, komut satırı araçları vb. oluşturmanıza izin vermek gibi birçok şey yapabilir. Express, kullanmanıza izin veren hafif bir çerçeveden başka bir şey değildir. Node/JS, Node'un düşük seviyeli ağı ve HTTP API'leriyle uğraşmak yerine web sunucuları oluşturmak için. Bir web sunucusu oluşturmak için Express'e ihtiyacınız yok.

Bu bölüme başlamadan önce, HTTP ve HTTP İstekleri (GET, POST, vb.) hakkında bilginiz yoksa, bir önceki makalemin yukarıda bağlantısı verilen ilgili bölümünü okumanızı tavsiye ederim.

Express'i kullanarak, HTTP İsteklerinin yapılabileceği farklı rotaların yanı sıra, bu rotaya bir istek yapıldığında tetiklenecek olan ilgili uç noktaları (geri çağırma işlevleri olan) ayarlayacağız. Rotalar ve uç noktalar şu anda anlamsızsa endişelenmeyin - bunları daha sonra açıklayacağım.

Diğer makalelerden farklı olarak, tüm kod tabanını tek bir parçaya atıp daha sonra açıklamak yerine, kaynak kodu ilerledikçe satır satır yazma yaklaşımını izleyeceğim. Bir terminal açarak başlayalım (Windows'ta Git Bash'in üstünde Terminus kullanıyorum - bu, Linux Alt Sistemini kurmadan Bash Shell isteyen Windows kullanıcıları için güzel bir seçenek), projemizin ortak plakasını ayarlayarak ve onu açarak başlayalım. Visual Studio Kodunda.

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

server.js dosyasının içinde, request require() işlevini kullanarak express gerektirerek başlayacağım.

 const express = require('express');

require('express') , Node'a daha önce kurduğumuz ve şu anda node_modules klasörünün içinde bulunan Express modülünü almasını söyler (çünkü npm install bunu yapar - bir node_modules klasörü oluşturur ve modülleri ve bağımlılıklarını oraya koyar). Geleneksel olarak ve Express ile uğraşırken, herhangi bir şey olarak adlandırılabilse de, require('express') express dönüş sonucunu tutan değişkeni çağırırız.

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”.

Bağlantı Noktası Numarası, bir sunucuda çalışan belirli bir hizmeti tanımlar. Bir aygıta uzaktan kabuk erişimine izin veren SSH veya Secure Shell, genellikle 22 numaralı bağlantı noktasında çalışır. FTP veya Dosya Aktarım Protokolü (örneğin, statik varlıkları bir sunucuya aktarmak için bir FTP İstemcisi ile kullanılabilir) genellikle Liman 21. O halde, yukarıdaki benzetmemizde limanların her evin içinde belirli odalar olduğunu söyleyebiliriz, çünkü evlerdeki odalar farklı şeyler için yapılmıştır - uyumak için bir yatak odası, yemek hazırlamak için bir mutfak, söz konusu tüketim için bir yemek odası. yiyecek vb., tıpkı bağlantı noktaları gibi belirli hizmetleri gerçekleştiren programlara karşılık gelir. Bizim için, Web Sunucuları genellikle 80 numaralı bağlantı noktasında çalışır, ancak başka bir hizmet tarafından kullanılmadıkları sürece (çakışamazlar) istediğiniz Bağlantı Noktası Numarasını belirtmekte özgürsünüz.

Bir web sitesine erişmek için sitenin IP Adresine ihtiyacınız vardır. Buna rağmen, normalde web sitelerine bir URL üzerinden erişiriz. Perde arkasında, bir DNS veya Etki Alanı Adı Sunucusu, bu URL'yi bir IP Adresine dönüştürerek tarayıcının sunucuya bir GET İsteği yapmasına, HTML'yi almasına ve ekrana işlemesine izin verir. 8.8.8.8 , Google'ın Genel DNS Sunucularından birinin adresidir. Uzak bir DNS Sunucusu aracılığıyla bir ana bilgisayar adının bir IP Adresine çözümlenmesini gerektirmenin zaman alacağını düşünebilirsiniz ve haklısınız. Gecikmeyi azaltmak için İşletim Sistemlerinde bir DNS Önbelleği bulunur - DNS arama bilgilerini depolayan ve böylece söz konusu aramaların yapılma sıklığını azaltan geçici bir veritabanı. DNS Çözümleyici Önbelleği, Windows'ta ipconfig /displaydns CMD komutuyla görüntülenebilir ve ipconfig /flushdns komutuyla temizlenebilir.

Bir Unix Sunucusunda, 80 gibi daha yaygın olan daha düşük numaralı bağlantı noktaları, kök düzeyi (bir Windows arka planından geliyorsanız yükseltilir ) ayrıcalıkları gerektirir. Bu nedenle, geliştirme çalışmalarımız için bağlantı noktası 3000'i kullanacağız, ancak üretim ortamımıza dağıttığımızda sunucunun bağlantı noktası numarasını (mevcut olan her şeyi) seçmesine izin vereceğiz.

Son olarak, IP Adreslerini doğrudan Google Chrome'un arama çubuğuna yazabileceğimizi ve böylece DNS Çözümleme mekanizmasını atlayabileceğimizi unutmayın. Örneğin 216.58.194.36 yazmak sizi Google.com'a götürür. Geliştirme ortamımızda, geliştirici sunucumuz olarak kendi bilgisayarımızı kullanırken, localhost ve port 3000 kullanacağız. Bir adres hostname:port olarak biçimlendirilir, bu nedenle sunucumuz localhost:3000 üzerinde olacaktır. Localhost veya 127.0.0.1 , geridöngü adresidir ve "bu bilgisayarın" adresi anlamına gelir. Bu bir ana bilgisayar adıdır ve IPv4 adresi 127.0.0.1 olarak çözülür. Şu anda makinenizde localhost'a ping atmayı deneyin. IPv6 geri döngü adresi olan ::1 geri veya IPv4 geri döngü adresi olan 127.0.0.1 geri alabilirsiniz. IPv4 ve IPv6, farklı standartlarla ilişkili iki farklı IP Adresi biçimidir - bazı IPv6 adresleri IPv4'e dönüştürülebilir, ancak tümü değil.

Eksprese Dönüş

HTTP İstekleri, Fiiller ve Durum Kodlarından bahsetmiştim, Düğümle Başlarken: API'lere Giriş, HTTP ve ES6+ JavaScript'te. Protokol hakkında genel bir anlayışınız yoksa, o parçanın “HTTP ve HTTP İstekleri” bölümüne geçmekten çekinmeyin.

Express için bir fikir edinmek için, veritabanında gerçekleştireceğimiz dört temel işlem için uç noktalarımızı ayarlayacağız - topluca CRUD olarak bilinen Oluştur, Oku, Güncelle ve Sil.

Uç noktalara URL'deki yollarla eriştiğimizi unutmayın. Diğer bir deyişle, "rota" ve "uç nokta" sözcükleri genellikle birbirinin yerine kullanılsa da, nokta teknik olarak bazı sunucu tarafı işlemlerini gerçekleştiren bir programlama dili işlevidir (ES6 Ok İşlevleri gibi), rota ise uç noktanın arkasında bulunduğu şeydir. arasında . Bu uç noktaları, istemciden uç noktanın arkasında yaşadığı rotaya uygun istek yapıldığında Express'in etkinleşeceği geri arama işlevleri olarak belirtiriz. Bir işlevi yerine getirenin uç noktalar olduğunu ve uç noktalara erişmek için kullanılan adın rota olduğunu fark ederek yukarıdakileri hatırlayabilirsiniz. Göreceğimiz gibi, aynı rota, farklı HTTP Fiilleri kullanılarak birden çok uç noktayla ilişkilendirilebilir (Polimorfizm ile klasik bir OOP arka planından geliyorsanız, yöntem aşırı yüklemesine benzer).

Müşterilerin sunucumuza istekte bulunmalarına izin vererek REST (Representational State Transfer) Mimarisini takip ettiğimizi unutmayın. Sonuçta bu bir REST veya RESTful API'dir. Belirli rotalara yapılan özel istekler , belirli şeyleri yapacak belirli uç noktaları tetikleyecektir . Bir uç noktanın yapabileceği böyle bir "şey" örneği, bir veritabanına yeni veriler eklemek, verileri kaldırmak, verileri güncellemek vb.

Express, hangi uç noktanın tetikleneceğini bilir, çünkü ona açıkça istek yöntemini (GET, POST, vb.) ve rotayı söyleriz — yukarıdakilerin belirli kombinasyonları için hangi işlevlerin tetikleneceğini tanımlarız ve müşteri, bir istekte bulunur. yol ve yöntem. Bunu daha basit bir şekilde ifade etmek gerekirse, Node ile Express'e - "Hey, birisi bu rotaya bir GET İsteği gönderirse, devam edin ve bu işlevi çalıştırın (bu uç noktayı kullanın)" diyeceğiz. İşler daha karmaşık hale gelebilir: “Express, eğer biri bu rotaya bir GET İsteği gönderirse, ancak talebinin başlığında geçerli bir Yetkilendirme Sahibi Simgesi göndermezse, lütfen bir HTTP 401 Unauthorized ile yanıt verin. Geçerli bir Taşıyıcı Simgesine sahiplerse, lütfen son noktayı ateşleyerek aradıkları korunan kaynağı gönderin. Çok teşekkürler ve iyi günler.” Gerçekten de, programlama dilleri belirsizliği sızdırmadan bu kadar yüksek seviyede olabilseydi güzel olurdu, ancak yine de temel kavramları gösterir.

Unutmayın, uç nokta bir şekilde rotanın arkasında bulunur. Bu nedenle, Express'in ne yapacağını anlayabilmesi için istemcinin isteğin başlığında hangi yöntemi kullanmak istediğini sağlaması zorunludur. İstek, istemcinin sunucuyla iletişim kurarken belirleyeceği (istek türüyle birlikte) belirli bir rotaya yapılacak ve Express'in yapması gerekeni yapmasına ve Express geri aramalarımızı tetiklediğinde yapmamız gerekeni yapmamıza izin verecek. . İşte her şey bundan ibaret.

Daha önceki kod örneklerinde, app üzerinde mevcut olan listen fonksiyonunu çağırdık, ona bir port ve geri çağrı ilettik. app kendisi, hatırlarsanız, express değişkeni bir işlev olarak çağırmanın geri dönüş sonucudur (yani, express() ) ve express değişkeni, node_modules klasörümüzden 'express' gerektirmenin dönüş sonucunu adlandırdığımız şeydir. app listen çağrılması gibi, HTTP İstek Bitiş Noktalarını da onları app çağırarak belirtiriz. GET'e bakalım:

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

İlk parametre bir string ve bitiş noktasının arkasında yaşayacağı yoldur. Geri arama işlevi, bitiş noktasıdır. Bunu tekrar söyleyeceğim: geri çağırma işlevi - ikinci parametre - ilk argüman olarak belirlediğimiz herhangi bir rotaya bir HTTP GET İsteği yapıldığında tetiklenecek son noktadır ( bu durumda /my-test-route ).

Şimdi, Express ile daha fazla iş yapmadan önce, rotaların nasıl çalıştığını bilmemiz gerekiyor. Dize olarak belirttiğimiz rota, www.domain.com/the-route-we-chose-earlier-as-a-string adresine istek yapılarak çağrılacaktır. Bizim durumumuzda, etki alanı localhost:3000 'dir, bu, yukarıdaki geri arama işlevini başlatmak için localhost:3000/my-test-route bir GET İsteği yapmamız gerektiği anlamına gelir. Yukarıdaki ilk argüman olarak farklı bir dize kullansaydık, URL'nin JavaScript'te belirttiğimiz ile eşleşmesi için farklı olması gerekirdi.

Bu tür şeylerden bahsederken, muhtemelen Glob Patterns'i duyacaksınız. API'mizin tüm rotalarının localhost:3000/** Glob Pattern'de bulunduğunu söyleyebiliriz, burada ** herhangi bir dizin veya alt dizini ifade eden bir joker karakterdir (rotaların dizin olmadığını unutmayın) kökün ebeveyn olduğu — yani her şey.

Devam edelim ve bu geri çağırma işlevine bir günlük ifadesi ekleyelim, böylece hep birlikte:

 // 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}.`) });

Projenin kök dizininde node server/server.js (sistemimizde kurulu olan ve sistem ortam değişkenlerinden global olarak erişilebilir olan Node ile) yürüterek sunucumuzu çalışır hale getireceğiz. Daha önce olduğu gibi, konsolda sunucunun hazır olduğu mesajını görmelisiniz. Artık sunucu çalıştığına göre, bir tarayıcı açın ve URL çubuğunda localhost:3000 adresini ziyaret edin.

Cannot GET / bir hata mesajı ile karşılaşacaksınız. Geliştirici konsolunu görüntülemek için Chrome'da Windows'ta Ctrl + Shift + I tuşlarına basın. Orada bir 404 olduğunu görmelisiniz (Kaynak bulunamadı). Bu mantıklı - sunucuya yalnızca biri localhost:3000/my-test-route ziyaret ettiğinde ne yapacağını söyledik. Tarayıcının localhost:3000 oluşturacağı hiçbir şey yoktur (bu, eğik çizgi ile localhost:3000/ ile eşdeğerdir).

Sunucunun çalıştığı terminal penceresine bakarsanız, yeni veri olmaması gerekir. Şimdi tarayıcınızın URL çubuğunda localhost:3000/my-test-route adresini ziyaret edin. Aynı hatayı Chrome'un Konsolunda da görebilirsiniz (tarayıcı içeriği önbelleğe aldığı ve hala oluşturulacak HTML'si olmadığı için), ancak sunucu işleminin çalıştığı terminalinizi görüntülerseniz, geri arama işlevinin gerçekten etkinleştiğini görürsünüz. ve günlük mesajı gerçekten günlüğe kaydedildi.

Sunucuyu Ctrl + C ile kapatın.

Şimdi, Cannot GET / mesajını kaybedebilmemiz için, o rotaya bir GET İsteği yapıldığında tarayıcıya oluşturacak bir şey verelim. Daha önceki app.get() 'imizi alacağım ve geri çağırma işlevinde iki argüman ekleyeceğim. Unutmayın, ilettiğimiz geri arama işlevi, sahne arkasında Express tarafından çağrılmaktadır ve Express, istediği argümanları ekleyebilir. Aslında iki tane ekler (teknik olarak üç, ama bunu daha sonra göreceğiz) ve her ikisi de son derece önemli olsa da, şimdilik birincisini umursamıyoruz. İkinci argüman res , response kısaltması olarak adlandırılır ve buna ilk parametre olarak undefined ayarlayarak erişeceğim:

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

Yine, res argümanını istediğimiz gibi adlandırabiliriz, ancak res , Express ile uğraşırken gelenekseldir. res aslında bir nesnedir ve bunun üzerinde istemciye veri göndermek için farklı yöntemler vardır. Bu durumda, tarayıcının oluşturacağı HTML'yi geri göndermek için res üzerinde bulunan send(...) işlevine erişeceğim. Bununla birlikte, HTML'yi geri göndermekle sınırlı değiliz ve metin, JavaScript Nesnesi, akış (akışlar özellikle güzeldir) veya her neyse geri göndermeyi seçebiliriz.

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

Sunucuyu kapatıp tekrar açarsanız ve ardından tarayıcınızı /my-test-route yolunda yenilerseniz, HTML'nin işlendiğini göreceksiniz.

Chrome Geliştirici Araçları'nın Ağ Sekmesi, bu GET İsteğini başlıklarla ilgili olarak daha ayrıntılı olarak görmenize olanak tanır.

Bu noktada, bir istemci bir istekte bulunduktan sonra küresel olarak başlatılabilen işlevler olan Express Middleware hakkında bilgi edinmeye başlamamız bize iyi hizmet edecektir.

Ekspres Ara Yazılım

Express, uygulamanız için özel ara yazılım tanımlama yöntemleri sağlar. Gerçekten de, Express Middleware'in anlamı en iyi burada Express Docs'ta tanımlanmıştır)

Ara katman yazılımı işlevleri, istek nesnesine ( req ), yanıt nesnesine ( res ) ve uygulamanın istek-yanıt döngüsündeki sonraki ara katman yazılımı işlevine erişimi olan işlevlerdir. Sonraki ara katman işlevi, genellikle next adlı bir değişkenle gösterilir.

Ara yazılım işlevleri aşağıdaki görevleri gerçekleştirebilir:

  • Herhangi bir kodu yürütün.
  • İstek ve yanıt nesnelerinde değişiklik yapın.
  • İstek-yanıt döngüsünü sonlandırın.
  • Yığındaki sonraki ara katman işlevini çağırın.

Başka bir deyişle, bir ara katman işlevi, bizim (geliştiricinin) tanımlayabileceğimiz özel bir işlevdir ve Express'in isteği aldığı zaman ile uygun geri arama işlevimizin tetiklendiği zaman arasında bir aracı görevi görecektir. Örneğin, her istek yapıldığında günlüğe kaydedecek bir log işlevi yapabiliriz. Ayrıca, yığında nereye yerleştirdiğinize bağlı olarak, uç noktamız tetiklendikten sonra bu ara yazılım işlevlerini çalıştırmayı da seçebileceğimizi unutmayın - bunu daha sonra göreceğiz.

Özel ara katman yazılımını belirtmek için, onu bir fonksiyon olarak tanımlamalı ve onu app.use(...) içine geçirmeliyiz.

 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().

Hep birlikte, şimdi elimizde:

 // 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}.`) });

İstekleri tekrar tarayıcı üzerinden yaparsanız, artık ara katman işlevinizin tetiklendiğini ve zaman damgalarını günlüğe kaydettiğini görmelisiniz. Denemeyi teşvik etmek için next işleve yapılan çağrıyı kaldırmayı deneyin ve ne olduğunu görün.

Ara yazılım geri çağırma işlevi, req , res ve next olmak üzere üç bağımsız değişkenle çağrılır. req , daha önce GET İşleyicisini oluştururken atladığımız parametredir ve üstbilgiler, özel üstbilgiler, parametreler ve istemciden gönderilmiş olabilecek herhangi bir gövde (örn. bir POST İsteği ile yaparsınız). Burada ara katman yazılımından bahsettiğimizi biliyorum, ancak hem uç noktalar hem de ara katman işlevi req ve res ile çağrılıyor. req ve res , istemciden gelen tek bir istek kapsamında hem ara katman yazılımında hem de uç noktada aynı olacaktır (biri veya diğeri onu mutasyona uğratmadıkça). Bu, örneğin, SQL veya NoSQL Enjeksiyonları gerçekleştirmeyi amaçlayan karakterleri sıyırarak ve ardından güvenli req uç noktaya teslim ederek verileri sterilize etmek için bir ara yazılım işlevi kullanabileceğiniz anlamına gelir.

res , daha önce görüldüğü gibi, istemciye birkaç farklı yolla veri göndermenize izin verir.

next , yığında veya uç noktada sonraki ara katman yazılımı işlevini çağırmak için ara katman yazılımı işini bitirdiğinde yürütmeniz gereken bir geri çağırma işlevidir. Ara katman yazılımında çalıştırdığınız tüm zaman uyumsuz işlevlerin then bloğunda bunu çağırmanız gerekeceğini unutmayın. Zaman uyumsuz işleminize bağlı olarak, onu catch bloğunda çağırmak isteyebilir veya istemeyebilirsiniz. Diğer bir deyişle, myMiddleware işlevi, istemciden istek yapıldıktan sonra ancak isteğin uç nokta işlevi başlatılmadan önce çalışır. Bu kodu çalıştırdığımızda ve bir istek yaptığımızda, konsolda A GET Request was made to... önce Middleware has fired... mesajını görmelisiniz. next() öğesini çağırmazsanız, ikinci kısım asla çalışmaz - isteğe bağlı uç nokta işleviniz tetiklenmez.

Ayrıca, bu işlevi anonim olarak tanımlayabileceğimi de unutmayın (bağlanacağım bir kural):

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

JavaScript ve ES6'da yeni olan herkes için, yukarıdakilerin çalışma şekli hemen anlam ifade etmiyorsa, aşağıdaki örnek yardımcı olacaktır. Basitçe, argüman olarak başka bir geri arama işlevini ( next ) alan bir geri arama işlevi (anonim işlev) tanımlıyoruz. Bir fonksiyon argümanını alan bir fonksiyona Yüksek Dereceli Fonksiyon diyoruz. Aşağıdaki şekilde bakın - Ekspres Kaynak Kodunun perde arkasında nasıl çalışabileceğinin temel bir örneğini gösterir:

 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.');

İlk olarak myMiddleware argüman olarak alan use adını veriyoruz. myMiddleware kendi içinde üç bağımsız değişken alan bir işlevdir - req , res ve next . use içinde myMiddlware çağrılır ve bu üç bağımsız değişken iletilir. next , use tanımlanan bir işlevdir. myMiddleware , use yönteminde callback olarak tanımlanır. Bu örnekte use öğesini app adlı bir nesneye yerleştirmiş olsaydım, herhangi bir soket veya ağ bağlantısı olmasa da Express'in kurulumunu tamamen taklit edebilirdik.

Bu durumda, hem myMiddleware hem de callback , işlevleri argüman olarak aldıkları için Yüksek Sıralı İşlevlerdir.

Bu kodu çalıştırırsanız, aşağıdaki yanıtı göreceksiniz:

 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.

Aynı sonucu elde etmek için anonim işlevleri de kullanabileceğimi unutmayın:

 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.');

Bu umarız çözüldüğünde, şimdi elimizdeki asıl göreve dönebiliriz - ara katman yazılımımızı kurma.

İşin aslı, genellikle bir HTTP İsteği aracılığıyla veri göndermeniz gerekecek. Bunu yapmak için birkaç farklı seçeneğiniz vardır: URL Sorgu Parametrelerini göndermek, daha önce öğrendiğimiz req nesnesinden erişilebilecek verileri göndermek vb. Bu nesne yalnızca app.use() , aynı zamanda herhangi bir uç noktaya. HTML'yi istemciye geri göndermek için res odaklanabilmemiz için daha önce dolgu maddesi olarak undefined kullandık, ancak şimdi ona erişmemiz gerekiyor.

 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. });

HTTP POST İstekleri, sunucuya bir gövde nesnesi göndermemizi gerektirebilir . İstemcide bir formunuz varsa ve kullanıcının adını ve e-postasını alırsanız, bu verileri büyük olasılıkla isteğin gövdesindeki sunucuya gönderirsiniz.

Bunun istemci tarafında nasıl görünebileceğine bir göz atalım:

 <!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>

Sunucu tarafında:

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

Kullanıcının adına ve e-postasına erişmek için belirli bir ara katman yazılımı kullanmamız gerekecek. Bu, verileri req üzerinde mevcut olan body adlı bir nesneye koyacaktır. Body Ayrıştırıcı, bunu yapmanın popüler bir yöntemiydi ve Express geliştiricileri tarafından bağımsız bir NPM modülü olarak sağlandı. Şimdi, Express bunu yapmak için kendi ara yazılımıyla önceden paketlenmiş olarak geliyor ve biz buna şöyle diyeceğiz:

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

Şimdi şunları yapabiliriz:

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

Tüm bunlar, istemciden gönderilen herhangi bir kullanıcı tanımlı girdiyi almak ve bunları req öğesinin body nesnesinde kullanılabilir hale getirmektir. req.body üzerinde, artık HTML'deki input etiketlerinin adları olan nameInput ve emailInput sahip olduğumuzu unutmayın. Şimdi, bu müşteri tanımlı veriler tehlikeli olarak kabul edilmelidir (asla, asla müşteriye güvenmeyin) ve sterilize edilmesi gerekir, ancak bunu daha sonra ele alacağız.

Express tarafından sağlanan başka bir ara katman yazılımı türü express.json() 'dur. express.json, istemciden gelen bir istekte gönderilen tüm JSON Yüklerini express.json üzerine req.body için kullanılırken, express.urlencoded , gelen istekleri dizeler, diziler veya diğer URL Encoding verileriyle req.body üzerine paketler. Kısacası, her ikisi de req.body manipüle eder, ancak .json() JSON Yükleri içindir ve .urlencoded() , diğerlerinin yanı sıra POST Sorgu Parametreleri içindir.

Bunu söylemenin başka bir yolu, Content-Type: application/json başlığına sahip gelen isteklerin ( fetch API'si ile bir POST Gövdesi belirtmek gibi) express.json() tarafından işlenirken Content-Type: application/x-www-form-urlencoded başlığına sahip isteklerin ele alınmasıdır. Content-Type: application/x-www-form-urlencoded (HTML Formları gibi) express.urlencoded() ile işlenecektir. Bu umarım şimdi mantıklıdır.

MongoDB İçin CRUD Rotalarımıza Başlıyoruz

Not : Bu makalede YAMA İstekleri gerçekleştirirken, JSONPatch RFC Spesifikasyonunu izlemeyeceğiz - bu serinin sonraki makalesinde düzelteceğimiz bir sorun.

Her bitiş noktasını app üzerinde ilgili işlevi çağırarak, ona route ve request ve response nesnelerini içeren bir callback fonksiyonunu geçirerek belirttiğimizi anladığımızı düşünürsek Bookshelf API için CRUD Routes tanımlamaya başlayabiliriz. Gerçekten de ve bunun bir giriş makalesi olduğunu düşünürsek, HTTP ve REST spesifikasyonlarını tam olarak takip etmeyeceğim ve mümkün olan en temiz mimariyi kullanmaya çalışmayacağım. Bu gelecekteki bir makalede gelecek.

Şimdiye kadar kullandığımız server.js dosyasını açacağım ve aşağıdaki temiz sayfadan başlamak için her şeyi boşaltacağım:

 // 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}.`));

Yukarıdaki dosyanın // ... bölümünü almak için aşağıdaki tüm kodları göz önünde bulundurun.

Uç noktalarımızı tanımlamak için ve bir REST API oluşturduğumuz için, rotaları adlandırmanın doğru yolunu tartışmalıyız. Yine daha fazla bilgi için bir önceki yazımın HTTP bölümüne göz atmalısınız. Kitaplarla uğraşıyoruz, bu nedenle tüm yollar /books arkasında yer alacaktır (çoğul adlandırma kuralı standarttır).

İstek Güzergah
İLETİ /books
ELDE ETMEK /books/id
YAMA /books/id
SİLMEK /books/id

Gördüğünüz gibi, bir kitabı POST yaparken bir kimliğin belirtilmesi gerekmez çünkü biz (veya daha doğrusu MongoDB), onu bizim için sunucu tarafında otomatik olarak oluşturacağız. Kitapların ALINMASI, YAMA YAPILMASI ve SİLMESİNİN tümü, bu kimliği daha sonra tartışacağımız uç noktamıza aktarmamızı gerektirecektir. Şimdilik, basitçe uç noktaları oluşturalım:

 // 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}`); });

:id sözdizimi, Express'e id URL'de aktarılacak dinamik bir parametre olduğunu söyler. Buna req üzerinde bulunan params nesnesinden erişebiliriz. “İstek üzerine req var” sihir ve sihir gibi sesler (ki bu mevcut değildir) programlamada tehlikelidir, biliyorum, ancak Express'in bir kara kutu olmadığını hatırlamanız gerekir. Bir MIT Lisansı altında GitHub'da bulunan açık kaynaklı bir projedir. Dinamik sorgu parametrelerinin req nesnesine nasıl yerleştirildiğini görmek istiyorsanız, kaynak kodunu kolayca görüntüleyebilirsiniz.

Hep birlikte, artık server.js dosyamızda aşağıdakiler var:

 // 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}.`));

Devam edin ve sunucuyu başlatın, node server.js terminalden veya komut satırından çalıştırın ve tarayıcınızı ziyaret edin. Chrome Geliştirme Konsolu'nu açın ve URL (Uniform Resource Locator) Çubuğunda localhost:3000/books adresini ziyaret edin. İşletim sisteminizin terminalinde, sunucunun çalıştığını gösteren göstergeyi ve GET için günlük ifadesini zaten görmelisiniz.

Şimdiye kadar, GET İsteklerini gerçekleştirmek için bir web tarayıcısı kullanıyoruz. Bu, yeni başlamak için iyidir, ancak API rotalarını test etmek için daha iyi araçların mevcut olduğunu çabucak bulacağız. Aslında, fetch çağrılarını doğrudan konsola yapıştırabilir veya bazı çevrimiçi hizmetleri kullanabiliriz. Bizim durumumuzda zaman kazanmak için cURL ve Postman kullanacağız. Bu yazıda her ikisini de kullanıyorum (her ikisinden birini kullanabilirsiniz, ancak kullanmadıysanız), onları tanıtabilmem için. cURL , çeşitli protokoller kullanarak veri aktarmak için tasarlanmış bir kitaplık (çok, çok önemli bir kitaplık) ve komut satırı aracıdır. Postacı, API'leri test etmek için GUI tabanlı bir araçtır. İşletim sisteminizde her iki araç için de ilgili kurulum talimatlarını uyguladıktan sonra sunucunuzun çalıştığından emin olun ve ardından yeni bir terminalde aşağıdaki komutları (tek tek) yürütün. Bunları tek tek yazıp yürütmeniz ve ardından günlük mesajını sunucunuzdan ayrı bir terminalde izlemeniz önemlidir. Ayrıca, standart programlama dili yorum sembolünün // Bash veya MS-DOS'ta geçerli bir sembol olmadığını unutmayın. Bu satırları atlamanız gerekecek ve ben onları burada yalnızca her cURL komut bloğunu açıklamak için kullanıyorum.

 // 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

Gördüğünüz gibi, URL Parametresi olarak iletilen kimlik herhangi bir değer olabilir. -X bayrağı, HTTP İsteğinin türünü belirtir (GET için atlanabilir) ve isteğin bundan sonra yapılacağı URL'yi sağlarız. Her isteği üç kez çoğaltarak, localhost ana bilgisayar adını, localhost'un çözdüğü IPv4 Adresini ( 127.0.0.1 ) veya localhost çözdüğü IPv6 Adresini ( ::1 ) kullansanız da her şeyin hala çalıştığını görmenizi localhost . . cURL IPv6 Adreslerinin köşeli parantez içine alınmasını gerektirdiğini unutmayın.

Artık iyi bir yerdeyiz - rotalarımızın ve son noktalarımızın basit yapısına sahibiz. Sunucu düzgün çalışır ve beklediğimiz gibi HTTP İsteklerini kabul eder. Beklediğinizin aksine, bu noktada gidecek çok uzun bir şey yok - sadece veritabanımızı kurmamız, barındırmamız (bir Hizmet Olarak Veritabanı - MongoDB Atlas kullanarak) ve verileri ona devam ettirmemiz (ve doğrulama gerçekleştirin ve hata yanıtları oluşturun).

Bir Üretim MongoDB Veritabanını Kurma

Bir üretim veritabanı oluşturmak için MongoDB Atlas Ana Sayfasına gideceğiz ve ücretsiz bir hesap için kaydolacağız. Ardından yeni bir küme oluşturun. Geçerli bir ücret katmanı seçerek varsayılan ayarları koruyabilirsiniz. Ardından “Küme Oluştur” düğmesine basın. Kümenin oluşturulması biraz zaman alacak ve ardından veritabanı URL'nizi ve şifrenizi elde edebileceksiniz. Bunları gördüğünüzde not alın. Bunları şimdilik sabit kodlayacağız ve daha sonra güvenlik amacıyla bunları ortam değişkenlerinde saklayacağız. Bir küme oluşturma ve kümeye bağlanma konusunda yardım için, sizi özellikle bu sayfa ve bu sayfa olmak üzere MongoDB Belgelerine yönlendireceğim veya aşağıya bir yorum bırakabilirsiniz ve yardımcı olmaya çalışacağım.

Bir Firavun Faresi Modeli Oluşturma

NoSQL (Yalnızca SQL Değil — Yapılandırılmış Sorgu Dili) bağlamında Belgelerin ve Koleksiyonların anlamlarını anlamanız önerilir. Referans olarak, önceki makalemin hem Mongoose Hızlı Başlangıç ​​Kılavuzunu hem de MongoDB bölümünü okumak isteyebilirsiniz.

Artık CRUD İşlemlerini kabul etmeye hazır bir veritabanımız var. Mongoose, bu işlemleri gerçekleştirmemize (bazı karmaşıklıkları soyutlayarak) ve ayrıca veritabanı koleksiyonunun şemasını veya yapısını kurmamıza izin verecek bir Düğüm modülüdür (veya ODM — Object Document Mapper).

Önemli bir sorumluluk reddi beyanı olarak, ORM'ler ve Aktif Kayıt veya Veri Eşleyici gibi kalıplar hakkında pek çok tartışma vardır. Bazı geliştiriciler ORM'ler tarafından yemin eder ve diğerleri onlara karşı yemin eder (yoluna çıktıklarına inanırlar). Ayrıca, ORM'lerin bağlantı havuzu oluşturma, soket bağlantıları ve işleme vb. gibi çok fazla soyut olduğunu unutmamak önemlidir. MongoDB Native Driver'ı (başka bir NPM Modülü) kolayca kullanabilirsiniz, ancak çok daha fazla iş çıkarır. ORM'leri kullanmadan önce Yerel Sürücü ile oynamanız tavsiye edilirken, kısa olması için Yerel Sürücüyü burada atlıyorum. Bir İlişkisel Veritabanındaki karmaşık SQL işlemleri için, tüm ORM'ler sorgu hızı için optimize edilmeyecektir ve sonunda kendi ham SQL'inizi yazabilirsiniz. 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. Title

  2. ISBN Number

  3. Yazar

    1. İlk adı

    2. Soy isim

  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. İşte bir örnek:

 // ... 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 });

Bu veritabanına bağlanacaktır. MongoDB Atlas panosundan elde ettiğimiz URL'yi sağlarız ve ikinci parametre olarak iletilen nesne, diğerlerinin yanı sıra kullanımdan kaldırma uyarılarını önlemek için kullanılacak özellikleri belirtir.

Sahne arkasında çekirdek MongoDB Native Driver'ı kullanan Mongoose, sürücüde yapılan son değişikliklere ayak uydurmaya çalışmak zorunda. Sürücünün yeni bir sürümünde, bağlantı URL'lerini ayrıştırmak için kullanılan mekanizma değiştirildi, bu nedenle resmi sürücüde bulunan en son sürümü kullanmak istediğimizi belirtmek için useNewUrlParser: true bayrağını geçiyoruz.

Varsayılan olarak, veritabanınızdaki veriler için dizinler (ve bunlara “indeksler” değil “indeksler” denir) (ki bu makalede ele almayacağız) ayarlarsanız, Mongoose Yerel Sürücüden sağlanan ensureIndex() işlevini kullanır. MongoDB, bu işlevi createIndex() lehine kullanımdan kaldırdı ve bu nedenle, useCreateIndex bayrağını true olarak ayarlamak, Mongoose'a, sürücüden gelen createIndex() yöntemini kullanmasını söyleyecektir; bu, kullanımdan kaldırılmayan işlevdir.

Mongoose'un orijinal findOneAndUpdate sürümü (bir veri tabanındaki bir belgeyi bulma ve güncelleme yöntemidir), Yerel Sürücü sürümünden önce gelir. Yani, findOneAndUpdate() orijinal olarak bir Yerel Sürücü işlevi değildi, daha ziyade Mongoose tarafından sağlanan bir işlevdi, bu nedenle Mongoose, findOneAndUpdate işlevini oluşturmak için sürücü tarafından perde arkasında sağlanan findAndModify kullanmak zorunda kaldı. Sürücü şimdi güncellendiğinde, kendi böyle bir işlevini içerir, bu nedenle findAndModify kullanmamız gerekmez. Bu mantıklı gelmeyebilir ve sorun değil - bu, şeylerin ölçeğinde önemli bir bilgi parçası değil.

Son olarak, MongoDB eski sunucu ve motor izleme sistemini kullanımdan kaldırdı. Yeni yöntemi useUnifiedTopology: true ile kullanıyoruz.

Şimdiye kadar sahip olduğumuz şey, veritabanına bağlanmanın bir yolu. Ama şu var ki, ölçeklenebilir veya verimli değil. Bu API için birim testleri yazdığımızda, birim testleri kendi test veritabanlarında kendi test verilerini (veya fikstürlerini) kullanacaklardır. Bu nedenle, farklı amaçlar için bağlantılar oluşturabilmenin bir yolunu istiyoruz - bazıları test ortamları için (istediğimiz zaman açıp kapatabileceğimiz), bazıları geliştirme ortamları için ve diğerleri üretim ortamları için. Bunu yapmak için bir fabrika kuracağız. (Bunu daha önce hatırladınız mı?)

Mongo'ya Bağlanmak — Bir JS Fabrikasının Uygulamasını Oluşturmak

Aslında, Java Nesneleri JavaScript Nesnelerine hiç benzemez ve bu nedenle, daha sonra, Fabrika Tasarım Modelinden yukarıda bildiklerimiz uygulanmayacaktır. Bunu sadece geleneksel kalıbı göstermek için bir örnek olarak verdim. Java veya C# veya C++, vb.'de bir nesneye ulaşmak için bir sınıf başlatmamız gerekir. Bu, derleyiciye öbek üzerindeki nesne için bellek ayırmasını söyleyen new anahtar sözcüğüyle yapılır. C++'da bu bize, asılı işaretçiler veya bellek sızıntıları olmaması için kendimizi temizlememiz gereken nesneye bir işaretçi verir (C++ üzerine kurulu Düğüm/V8'in aksine, C++'da çöp toplayıcı yoktur) JavaScript'te, yukarıdakilerin yapılması gerekmez - bir nesneyi elde etmek için bir sınıf başlatmamız gerekmez - bir nesne yalnızca {} . Bazı insanlar JavaScript'teki her şeyin bir nesne olduğunu söyleyecektir, ancak bu teknik olarak doğru değildir çünkü ilkel türler nesne değildir.

Yukarıdaki nedenlerden dolayı, JS Fabrikamız daha basit olacak, bir fabrikanın bir nesneyi (bir JS nesnesi) döndüren bir işlev olduğu gevşek tanımına bağlı kalarak. Bir fonksiyon bir nesne olduğundan ( function prototip kalıtım yoluyla object miras aldığı için), aşağıdaki örneğimiz bu kriteri karşılayacaktır. Fabrikayı uygulamak için server içinde db adında yeni bir klasör oluşturacağım. db içinde mongoose.js adında yeni bir dosya oluşturacağım. Bu dosya veritabanına bağlantı yapacaktır. mongoose.js içinde, connectionFactory adlı bir işlev oluşturacağım ve varsayılan olarak dışa aktaracağım:

 // 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;

Yöntem imzasıyla aynı satırda bir ifade döndüren Ok İşlevleri için ES6 tarafından sağlanan kısayolu kullanarak, bu dosyayı connectionFactory tanımından kurtulup varsayılan olarak fabrikayı dışa aktararak daha basit hale getireceğim:

 // 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 });

Şimdi, tek yapmanız gereken dosyayı istemek ve dışa aktarılan yöntemi şu şekilde çağırmak:

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

MongoDB URL'nizin fabrika işlevine bir parametre olarak sağlanmasını sağlayarak kontrolü tersine çevirebilirsiniz, ancak URL'yi ortama dayalı bir ortam değişkeni olarak dinamik olarak değiştireceğiz.

Bağlantımızı bir işlev olarak yapmanın faydaları, üretime yönelik dosyalardan ve hem cihazda hem de uzak bir CI/CD ardışık düzeniyle yerel ve uzaktan entegrasyon testlerini hedefleyen dosyalardan veritabanına bağlanmak için bu işlevi daha sonra kodda çağırabilmemizdir. / sunucu oluştur.

Uç Noktalarımızı Oluşturma

Artık uç noktalarımıza çok basit CRUD ile ilgili mantık eklemeye başlıyoruz. Daha önce belirtildiği gibi, kısa bir sorumluluk reddi gereklidir. Burada iş mantığımızı uygularken izlediğimiz yöntemler, basit projelerden başka hiçbir şey için yansıtmanız gereken yöntemler değil . Veritabanlarına bağlanmak ve doğrudan uç noktalar içinde mantık yürütmek, uygulama genelinde bir yeniden düzenleme gerçekleştirmek zorunda kalmadan hizmetleri veya VTYS'leri değiştirme yeteneğinizi kaybettiğiniz için (ve böyle olmalıdır) hoş karşılanmaz. Yine de, bunun yeni başlayanlar için bir makale olduğunu göz önünde bulundurarak, bu kötü uygulamaları burada kullanıyorum. Bu serideki gelecekteki bir makale, mimarimizin hem karmaşıklığını hem de kalitesini nasıl artırabileceğimizi tartışacaktır.

Şimdilik server.js dosyamıza geri dönelim ve ikimizin de aynı başlangıç ​​noktasına sahip olduğumuzdan emin olalım. Not Veritabanı bağlantı fabrikamız için require ifadesini ekledim ve ./models/book.js dışa aktardığımız modeli içe aktardım.

 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}.`));

app.post() ile başlayacağım. Book modeline erişimimiz var çünkü onu içinde oluşturduğumuz dosyadan dışa aktardık. Mongoose belgelerinde belirtildiği gibi, Book yapılandırılabilir. Yeni bir kitap oluşturmak için kurucuyu çağırır ve kitap verilerini aşağıdaki gibi iletiriz:

 const book = new Book(bookData);

Bizim durumumuzda, istekte gönderilen nesne olarak bookData sahip olacağız ve req.body.book üzerinde mevcut olacak. Unutmayın, express.json() ara yazılımı, gönderdiğimiz herhangi bir JSON verisini req.body üzerine koyacaktır. JSON'u aşağıdaki biçimde göndereceğiz:

 { "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 } }

O halde bunun anlamı, atladığımız JSON'un ayrıştırılacağı ve tüm JSON nesnesinin (ilk parantez çifti) express.json() ara yazılımı tarafından req.body üzerine yerleştirileceğidir. JSON nesnemizdeki tek ve tek özellik book ve bu nedenle book nesnesi req.body.book üzerinde kullanılabilir olacaktır.

Bu noktada model kurucu fonksiyonunu çağırabilir ve verilerimizi iletebiliriz:

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

Burada birkaç şeye dikkat edin. Yapıcı işlevini çağırdıktan sonra geri döndüğümüz örnekte save yöntemini çağırmak, req.body.book nesnesini yalnızca ve ancak Mongoose modelinde tanımladığımız şema ile uyumluysa veritabanına devam ettirecektir. Verileri bir veritabanına kaydetme eylemi eşzamansız bir işlemdir ve bu save() yöntemi, yerleşmesini çok beklediğimiz bir söz verir. Bir .then() çağrısında zincirleme yapmak yerine, ES6 Async/Await sözdizimini kullanıyorum, bu da app.post async için geri arama işlevini yapmam gerektiği anlamına geliyor.

book.save() , istemcinin gönderdiği nesne tanımladığımız şemaya uymuyorsa ValidationError ile reddedecektir. Geçerli kurulumumuz, doğrulamayla ilgili bir hata olması durumunda uygulamamızın çökmesini istemediğimiz için, bazı çok düzensiz ve kötü yazılmış kodlar oluşturuyor. Bunu düzeltmek için, tehlikeli işlemi bir try/catch yan tümcesi ile çevreleyeceğim. Bir hata durumunda, bir HTTP 400 Hatalı İstek veya bir HTTP 422 İşlenemez Varlık döndüreceğim. Hangisinin kullanılacağı konusunda bir miktar tartışma var, bu yüzden daha genel olduğu için bu makale için 400 ile bağlı kalacağım.

 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' }); } });

res.send({ book }) ile başarılı durumda book nesnesini istemciye geri döndürmek için ES6 Object Shorthand kullandığıma dikkat edin - bu res.send({ book: book }) ile eşdeğer olacaktır. Ayrıca işlevimin çıktığından emin olmak için ifadeyi döndürüyorum. catch bloğunda, durumu açıkça 400 olarak ayarladım ve geri gönderilen nesnenin error özelliğinde 'ValidationError' dizesini döndürdüm. A 201, "OLUŞTURULDU" anlamına gelen başarı yolu durum kodudur.

Aslında, bu da en iyi çözüm değil çünkü başarısızlığın nedeninin müşteri tarafında bir Kötü İstek olduğundan gerçekten emin olamayız. Belki veritabanıyla bağlantıyı kaybettik (soket bağlantısının kesilmesi, dolayısıyla geçici bir istisna olması gerekiyordu), bu durumda muhtemelen bir 500 Dahili Sunucu hatası döndürmemiz gerekir. Bunu kontrol etmenin bir yolu, e error nesnesini okumak ve seçici olarak bir yanıt döndürmek olacaktır. Bunu şimdi yapalım, ancak defalarca söylediğim gibi, bir takip makalesi Yönlendiriciler, Denetleyiciler, Hizmetler, Depolar, özel hata sınıfları, özel hata ara yazılımı, özel hata yanıtları, Veritabanı Modeli/Etki Alanı Varlığı verileri açısından uygun mimariyi tartışacaktır. eşleme ve Komut Sorgusu Ayırma (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' }); } } });

Devam edin ve Postacı'yı açın (sahip olduğunuzu varsayarak, aksi takdirde indirin ve kurun) ve yeni bir istek oluşturun. localhost:3000/books için bir POST İsteği yapacağız. Postacı İsteği bölümündeki “Gövde” sekmesinin altında “raw” radyo düğmesini seçeceğim ve en sağdaki açılır düğmeden “JSON” u seçeceğim. Bu devam edecek ve Content-Type: application/json başlığını isteğe otomatik olarak ekleyecektir. Daha sonra Book JSON Nesnesini daha önce kopyalayıp Gövde metin alanına yapıştıracağım. Sahip olduğumuz şey bu:

Postacı GUI, POST İsteğinden gelen yanıt verileriyle dolduruldu.
POST İsteğimize JSON Yük Yanıtı. (Büyük önizleme)

Daha sonra gönder düğmesine basacağım ve Postacı'nın “Yanıt” bölümünde (alt sıra) 201 Oluşturuldu yanıtı görmelisiniz. Bunu görüyoruz çünkü Express'ten özellikle bir 201 ve Book nesnesiyle yanıt vermesini istedik — res.send() durum kodu olmadan yapsaydık, express otomatik olarak 200 OK ile yanıt verirdi. Gördüğünüz gibi, Book nesnesi artık veritabanına kaydedilmiştir ve POST İsteğine Yanıt olarak istemciye döndürülmüştür.

Postacı GUI, POST İsteği için verilerle dolduruldu.
POST İsteğimiz için Postacı alanlarını dolduracak veriler. (Büyük önizleme)

MongoDB Atlas aracılığıyla Kitap koleksiyonu veritabanını görüntülerseniz, kitabın gerçekten kaydedildiğini görürsünüz.

Ayrıca __v ve _id alanlarını eklediğini de söyleyebilirsiniz. İlki belgenin sürümünü temsil eder, bu durumda 0 ve ikincisi, belgenin MongoDB tarafından otomatik olarak oluşturulan ve düşük bir çakışma olasılığına sahip olduğu garanti edilen ObjectID'dir.

Şimdiye Kadar Ele Aldıklarımızın Bir Özeti

Bu yazıda şimdiye kadar çok şey anlattık. Express API'yi bitirmek için geri dönmeden önce kısa bir özeti gözden geçirerek kısa bir mola verelim.

ES6 Object Destructuring, ES6 Object Shorthand Syntax ve ES6 Rest/Spread operatörünü öğrendik. Bunların üçü de aşağıdakileri (ve yukarıda tartışıldığı gibi daha fazlasını) yapmamıza izin verir:

 // 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

Ayrıca Express, Expess Middleware, Sunucular, Bağlantı Noktaları, IP Adresleme vb. konuları da ele aldık. require('express')(); işlevinden gelen dönüş sonuçlarında mevcut yöntemlerin olduğunu öğrendiğimizde işler ilginçleşti. app.get ve app.post gibi HTTP Fiillerinin adlarıyla.

Bu require('express')() kısmı size mantıklı gelmediyse, yapmak istediğim nokta buydu:

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

Mongoose için bağlantı fabrikasını daha önce kovduğumuz gibi mantıklı olmalı.

Uç nokta işlevi (veya geri arama işlevi) olan her rota işleyicisi, sahne arkasında Express'ten bir req nesnesine ve bir res nesnesine iletilir. (Bir dakika next göreceğimiz gibi, teknik olarak da sıraya giriyorlar). req , başlıklar veya gönderilen herhangi bir JSON gibi istemciden gelen isteğe özel verileri içerir. res , müşteriye yanıtları döndürmemize izin veren şeydir. next işlev de işleyicilere aktarılır.

Mongoose ile veritabanına iki yöntemle nasıl bağlanabileceğimizi gördük - ilkel bir yol ve Fabrika Modelinden ödünç alan daha gelişmiş/pratik bir yol. Jest ile Birim ve Entegrasyon Testini (ve mutasyon testini) tartışırken bunu kullanacağız çünkü bu, kendisine karşı iddialarda bulunabileceğimiz tohum verileriyle doldurulmuş DB'nin bir test örneğini döndürmemize izin verecek.

Bundan sonra bir Mongoose şema nesnesi oluşturduk ve onu bir model oluşturmak için kullandık ve ardından yeni bir örneğini oluşturmak için o modelin yapıcısını nasıl çağırabileceğimizi öğrendik. Örnekte mevcut olan bir save yöntemi (diğerlerinin yanı sıra), doğası gereği asenkron olan ve ilettiğimiz nesne yapısının şemaya uygun olup olmadığını kontrol edecek, uygunsa sözü çözecek ve eğer söz konusu ise bir ValidationError ile sözü reddedecektir. o değil. Çözüm olması durumunda yeni belge veritabanına kaydedilir ve bir HTTP 200 OK/201 CREATED ile yanıt veririz, aksi takdirde uç noktamızda atılan hatayı yakalar ve istemciye bir HTTP 400 Hatalı İstek göndeririz.

Uç noktalarımızı oluşturmaya devam ederken, model ve model örneğinde bulunan bazı yöntemler hakkında daha fazla bilgi edineceksiniz.

Uç Noktalarımızı Bitirmek

POST Bitiş Noktasını tamamladıktan sonra GET'i ele alalım. Daha önce bahsettiğim gibi, rota içindeki :id sözdizimi, Express'in req.params id bir rota parametresi olduğunu bilmesini sağlar. Rotadaki “joker karakter” için bir kimlik eşleştirdiğinizde, ilk örneklerde ekrana yazdırıldığını zaten gördünüz. Örneğin, “/books/test-id-123” için bir GET İsteği yaptıysanız, o zaman req.params.id , test-id-123 dizesi olur çünkü param adı, rotanın HTTP GET /books/:id olmasıyla id idi. HTTP GET /books/:id .

Bu nedenle, tek yapmamız gereken o kimliği req nesnesinden almak ve veritabanımızdaki herhangi bir belgenin aynı kimliğe sahip olup olmadığını kontrol etmek - Mongoose (ve Yerel Sürücü) tarafından çok kolay hale getirilen bir şey.

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

Modelimiz üzerinde erişilebilir olanın, kimliğine göre bir belgeyi bulabilecek diyebileceğimiz bir fonksiyon olduğunu görebilirsiniz. Perde arkasında, Mongoose, findById kimliği belgedeki _id alanının türüne veya bu durumda bir ObjectId olarak yayınlayacaktır. Eşleşen bir kimlik bulunursa (ve ObjectId için yalnızca bir tane bulunacaksa, son derece düşük bir çarpışma olasılığına sahiptir), bu belge book sabit değişkenimize yerleştirilecektir. Değilse, book boş olacak - yakın gelecekte kullanacağımız bir gerçek.

Şimdilik, sunucuyu yeniden başlatalım ( nodemon kullanmıyorsanız sunucuyu yeniden başlatmanız gerekir) ve Books Koleksiyonu içinde daha önce tek kitap belgesine sahip olduğumuzdan emin olun. Devam edin ve aşağıdaki resmin vurgulanan kısmı olan o belgenin kimliğini kopyalayın:

Kitap Belgesi Nesne Kimliği
Yaklaşan GET İsteği için kullanılacak bir ObjectID örneği. (Büyük önizleme)

Ve bunu Postman ile /books/:id için aşağıdaki gibi bir GET İsteği yapmak için kullanın (gövde verilerinin daha önceki POST İsteğimden kaldığını unutmayın. Aşağıdaki resimde gösterilmesine rağmen aslında kullanılmıyor) :

Postacı GUI'si, GET İsteği için verilerle dolduruldu.
GET İsteği için API URL'si ve Postacı verileri. (Büyük önizleme)

Bunu yaptıktan sonra, belirtilen kimliğe sahip kitap belgesini Postacı yanıt bölümünün içine geri almalısınız. Daha önce, sunucuya yeni kaynakları "POST" veya "itmek" için tasarlanan POST Yolu ile, yeni bir kaynak (veya belge) oluşturulduğu için 201 Created ile yanıt verdiğimize dikkat edin. GET durumunda, yeni bir şey oluşturulmadı - sadece belirli bir kimliğe sahip bir kaynak istedik, bu nedenle 201 Oluşturuldu yerine 200 OK durum kodunu geri aldık.

Yazılım geliştirme alanında yaygın olduğu gibi, uç durumlar hesaba katılmalıdır - kullanıcı girişi doğası gereği güvensiz ve hatalıdır ve geliştiriciler olarak bizim işimiz, verilebilecek girdi türlerine karşı esnek olmak ve bunlara yanıt vermektir. buna göre. Kullanıcı (veya API Arayan) bize bir MongoDB ObjectID'ye dönüştürülemeyen bir kimlik veya yayınlanabilen ancak mevcut olmayan bir kimlik iletirse ne yaparız?

İlk durumda, Mongoose bir CastError - bu anlaşılabilir çünkü math-is-fun gibi bir kimlik sağlarsak, bu açıkça bir ObjectID'ye dönüştürülebilecek bir şey değildir ve bir ObjectID'ye yayın yapmak özellikle budur Mongoose kaputun altında yapıyor.

İkinci durumda, sorunu bir Null Check veya Guard Cümlesi ile kolayca düzeltebiliriz. Her iki durumda da, HTTP 404 Bulunamadı Yanıtını geri göndereceğim. Bunu yapmanın birkaç yolunu göstereceğim, kötü bir yol ve sonra daha iyi bir yol.

İlk olarak, şunları yapabilirdik:

 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' }); } });

Bu işe yarıyor ve onu gayet iyi kullanabiliriz. await Book.findById() ifadesinin, ID dizesi bir ObjectID'ye dönüştürülemezse, catch bloğunun yürütülmesine neden olan bir Mongoose CastError atmasını bekliyorum. Dökülebiliyorsa ancak karşılık gelen ObjectID mevcut değilse, book boş olacak ve null Check bir hata vererek tekrar catch bloğunu tetikleyecektir. catch içinde sadece bir 404 döndürüyoruz. Burada iki problem var. İlk olarak, Kitap bulunsa ancak başka bir bilinmeyen hata meydana gelse bile, muhtemelen müşteriye genel bir 500'ü yakalamamız gerektiğinde bir 404'ü geri göndeririz. İkincisi, gönderilen kimliğin geçerli olup olmadığını gerçekten ayırt etmiyoruz ancak var olmayan veya sadece kötü bir kimlik olup olmadığı.

Yani, işte başka bir yol:

 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' }); } } });

Bununla ilgili güzel olan şey, bir 400, bir 404 ve bir jenerik 500'ün üç durumunu da ele alabiliyor olmamızdır. Dikkat ederseniz, Null Check on book 'dan sonra, yanıtımda return anahtar sözcüğünü kullanıyorum. Bu çok önemli çünkü oradaki rota işleyicisinden çıktığımızdan emin olmak istiyoruz.

req.params mongoose.Types.ObjectId.isValid(' id mongoose.Types.ObjectId.isValid('id); , ancak bunun bazen beklenmedik şekilde çalışmasına neden olan 12 baytlık dizelere sahip bir uç durum vardır.

Söz konusu tekrarı örneğin bir HTTP Yanıt kitaplığı olan Boom ile daha az acı verici hale getirebiliriz veya Hata İşleme Ara Yazılımını kullanabiliriz. Ayrıca Mongoose Hatalarını, burada açıklandığı gibi Mongoose Hooks/Middleware ile daha okunabilir bir şeye dönüştürebiliriz. Ek bir seçenek de özel hata nesneleri tanımlamak ve global Express Error Handling Middleware'i kullanmak olabilir, ancak bunu daha iyi mimari yöntemleri tartışacağımız bir sonraki makale için kaydedeceğim.

PATCH /books/:id bitiş noktasında, söz konusu kitap için güncellemeleri içeren bir güncelleme nesnesinin iletilmesini bekleriz. Bu makale için tüm alanların güncellenmesine izin vereceğiz, ancak gelecekte belirli alanların güncellemelerine nasıl izin vermeyeceğimizi göstereceğim. Ek olarak, PATCH Uç Noktamızdaki hata işleme mantığının GET Uç Noktamızla aynı olacağını göreceksiniz. Bu, DRY İlkelerini ihlal ettiğimizin bir göstergesidir, ancak buna daha sonra değineceğiz.

Tüm güncellemelerin req.body updates updates ve güncellemeyi gerçekleştirmek için Book.findByAndUpdate işlevini özel bir bayrakla kullanmasını bekliyorum.

 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' }); } } });

Burada birkaç şeye dikkat edin. Önce req.params id updates req.body yok ederiz.

Book modelinde, söz konusu belgenin kimliğini, gerçekleştirilecek güncellemeleri ve isteğe bağlı seçenekler nesnesini alan findByIdAndUpdate adlı bir işlev bulunur. Normalde, Mongoose güncelleme işlemleri için yeniden doğrulama gerçekleştirmez, bu nedenle options nesnesi bunu yapmaya runValidators: true flag. Ayrıca, Mongoose 4'ten itibaren, Model.findByIdAndUpdate artık değiştirilen belgeyi değil, bunun yerine orijinal belgeyi döndürür. new: true bayrağı (varsayılan olarak false olan) bu davranışı geçersiz kılar.

Son olarak, diğerlerine oldukça benzeyen DELETE uç noktamızı oluşturabiliriz:

 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' }); } } });

Bununla ilkel API'miz tamamlandı ve tüm uç noktalara HTTP İstekleri yaparak test edebilirsiniz.

Mimarlık ve Nasıl Düzelteceğimiz Hakkında Kısa Bir Sorumluluk Reddi

Mimari açıdan, burada sahip olduğumuz kod oldukça kötü, dağınık, KURU değil, KATI değil, hatta iğrenç bile diyebilirsiniz. Bu "Rota İşleyicileri" olarak adlandırılanlar, sadece "rotaları teslim etmekten" çok daha fazlasını yapıyor - doğrudan veritabanımızla arayüz oluşturuyorlar. Bu kesinlikle hiçbir soyutlama olmadığı anlamına gelir.

Kabul edelim, çoğu uygulama asla bu kadar küçük olmayacak veya muhtemelen Firebase Veritabanı ile sunucusuz mimarilerden kurtulabilirsiniz. Belki daha sonra göreceğimiz gibi, kullanıcılar kitaplarından avatarlar, alıntılar ve snippet'ler vb. yükleme yeteneği istiyorlar. Belki WebSockets ile kullanıcılar arasında canlı sohbet özelliği eklemek istiyoruz ve hatta şunu söylemeye kadar gidelim. Kullanıcıların küçük bir ücret karşılığında birbirlerinden kitap ödünç almalarına izin vermek için uygulamamızı açacağız - bu noktada Stripe API ile Ödeme Entegrasyonunu ve Shippo API ile nakliye lojistiğini düşünmemiz gerekiyor.

Mevcut mimarimize devam ettiğimizi ve tüm bu işlevleri eklediğimizi varsayalım. Denetleyici Eylemleri olarak da bilinen bu rota taşıyıcılar, yüksek döngüsel karmaşıklıkla çok, çok büyük olacaklar. Böyle bir kodlama stili ilk günlerde bize uygun olabilir, ancak ya verilerimizin referans olduğuna karar verirsek ve bu nedenle PostgreSQL'in MongoDB'den daha iyi bir veritabanı seçimi olduğuna karar verirsek? Şimdi tüm uygulamamızı yeniden düzenlememiz, Mongoose'u çıkarmamız, Denetleyicilerimizi değiştirmemiz vb. tüm bunlar iş mantığının geri kalanında potansiyel hatalara yol açabilir. Bu tür başka bir örnek, AWS S3'ün çok pahalı olduğuna ve GCP'ye geçmek istediğimize karar vermek olabilir. Yine, bu uygulama çapında bir yeniden düzenleme gerektirir.

Etki Alanına Dayalı Tasarım, Komut Sorgusu Sorumluluğu Ayrımı ve Olay Kaynak Kullanımından Teste Dayalı Geliştirme, SOILD, Katmanlı Mimari, Soğan Mimarisi ve daha fazlasına kadar mimari hakkında birçok görüş olmasına rağmen, biz basit Katmanlı Mimariyi uygulamaya odaklanacağız. Denetleyiciler, Hizmetler ve Depolardan oluşan ve Kompozisyon, Bağdaştırıcılar/Sarmalayıcılar ve Dependency Injection ile Kontrolün Tersine Çevirilmesi gibi Tasarım Modellerini kullanan gelecekteki makaleler. Bu, bir dereceye kadar JavaScript ile gerçekleştirilebilse de, bu mimariyi elde etmek için TypeScript seçeneklerini de inceleyeceğiz ve Generics gibi OOP kavramlarına ek olarak Ya Monads gibi işlevsel programlama paradigmalarını kullanmamıza izin vereceğiz.

Şimdilik yapabileceğimiz iki küçük değişiklik var. Hata işleme mantığımız tüm uç noktaların catch bloğunda oldukça benzer olduğundan, onu yığının en sonundaki özel bir Express Error Handling Middleware işlevine ayıklayabiliriz.

Mimarimizi Temizlemek

Şu anda, tüm uç noktalarımızda çok büyük miktarda hata işleme mantığını tekrarlıyoruz. Bunun yerine, bir hata ile çağrılan bir Ekspres Ara Yazılım İşlevi olan bir Ekspres Hata İşleme Ara Yazılım işlevi, req ve res nesneleri ve sonraki işlev oluşturabiliriz.

Şimdilik, bu ara katman işlevini oluşturalım. Tek yapacağım, alıştığımız aynı hata işleme mantığını tekrarlamak:

 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' }); } });

Bu, Mongoose Errors ile çalışmıyor gibi görünüyor, ancak genel olarak, hata örneklerini belirlemek için if/else if/else kullanmak yerine, hatanın yapıcısını değiştirebilirsiniz. Ancak elimizdekileri bırakacağım.

Senkronize bir uç nokta/rota işleyicisinde, bir hata atarsanız, Express bunu yakalar ve sizin tarafınızdan fazladan bir iş gerekmeden işleme koyar. Ne yazık ki, bizim için durum böyle değil. Asenkron kodla uğraşıyoruz. Hata işlemeyi zaman uyumsuz rota işleyicileri ile Express'e devretmek için, hatayı kendimiz yakalar ve next() öğesine iletiriz.

Bu nedenle, next'in bitiş noktasına üçüncü argüman olmasına izin vereceğim ve hata örneğini next öğesine geçirmek için catch bloklarındaki hata işleme mantığını kaldıracağım next

 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) } });

Bunu tüm rota işleyicilere yaparsanız, aşağıdaki kodu elde etmelisiniz:

 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}.`));

Daha da ileri gidersek, ara yazılımı ele alma hatamızı başka bir dosyaya ayırmaya değer olabilir, ancak bu önemsizdir ve bunu bu serinin ilerideki makalelerinde göreceğiz. Ek olarak, catch bloğunda bir sonrakini aramak zorunda kalmamamıza izin vermek için express-async-errors adlı bir NPM modülü kullanabiliriz, ancak yine, size işlerin resmi olarak nasıl yapıldığını göstermeye çalışıyorum.

CORS ve Aynı Menşe Politikası Hakkında Bir Kelime

Web sitenize myWebsite.com alan adından hizmet verildiğini, ancak sunucunuzun myOtherDomain.com/api adresinde olduğunu varsayalım. CORS, Kaynaklar Arası Kaynak Paylaşımı anlamına gelir ve etki alanları arası isteklerin gerçekleştirilebileceği bir mekanizmadır. Yukarıdaki durumda, sunucu ve ön uç JS kodu farklı etki alanlarında olduğundan, güvenlik nedenleriyle tarayıcı tarafından yaygın olarak kısıtlanan ve belirli HTTP üstbilgileri sağlayarak hafifletilen iki farklı kaynaktan bir istekte bulunursunuz.

Aynı Menşe Politikası, yukarıda belirtilen kısıtlamaları gerçekleştiren şeydir - bir web tarayıcısı, yalnızca aynı Menşe üzerinden yapılması gerekenlere izin verir.

CORS ve SOP'ye daha sonra React ile Kitap API'miz için bir Web paketi paketlenmiş ön uç oluşturduğumuzda değineceğiz.

Sonuç Ve Sırada Ne Var

Bu yazıda çok tartıştık. Belki tamamen pratik değildi, ancak umarız Express ve ES6 JavaScript özellikleriyle daha rahat çalışmanızı sağlamıştır. Programlamada yeniyseniz ve Düğüm, başladığınız ilk yolsa, umarım Java, C++ ve C# gibi statik türdeki dillere yapılan referanslar, JavaScript ile statik karşılıkları arasındaki bazı farklılıkları vurgulamaya yardımcı olmuştur.

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
  • Hizmetler
  • depolar
  • 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
  • doğrulama
  • Decorators
  • Logging and Logging Levels
  • Unit Tests, Integration Tests (E2E), and Mutation Tests
  • The Structured Query Language
  • Relations
  • HTTP/Express Security Best Practices
  • Node Best Practices
  • OWASP Security Best Practices
  • Ve dahası.

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.