Node.js/TypeScript REST API Oluşturma, Bölüm 1: Express.js

Yayınlanan: 2022-03-11

Node.js'de Nasıl REST API Yazarım?

REST API için bir arka uç oluştururken, Express.js genellikle Node.js çerçeveleri arasında ilk tercihtir. Statik HTML ve şablonlar oluşturmayı da desteklerken, bu seride TypeScript kullanarak arka uç geliştirmeye odaklanacağız. Ortaya çıkan REST API, herhangi bir ön uç çerçevesinin veya harici arka uç hizmetinin sorgulayabileceği bir API olacaktır.

İhtiyacın olacak:

  • Temel JavaScript ve TypeScript bilgisi
  • Node.js hakkında temel bilgiler
  • Temel REST mimarisi bilgisi (gerekirse önceki REST API makalemin bu bölümüne bakın)
  • Node.js'nin hazır kurulumu (tercihen 14+ sürümü)

Bir terminalde (veya komut isteminde), proje için bir klasör oluşturacağız. Bu klasörden npm init çalıştırın. Bu, ihtiyacımız olan bazı temel Node.js proje dosyalarını oluşturacaktır.

Ardından, Express.js çerçevesini ve bazı yararlı kitaplıkları ekleyeceğiz:

 npm i express debug winston express-winston cors

Bu kitaplıkların Node.js geliştiricilerinin favorileri olmasının iyi nedenleri vardır:

  • debug , uygulamamızı geliştirirken console.log() çağırmamak için kullanacağımız bir modüldür. Bu şekilde, sorun giderme sırasında hata ayıklama ifadelerini kolayca filtreleyebiliriz. Ayrıca manuel olarak çıkarılmaları yerine üretimde tamamen kapatılabilirler.
  • Winston, winston yapılan isteklerin ve döndürülen yanıtların (ve hataların) günlüğe kaydedilmesinden sorumludur. express-winston , doğrudan Express.js ile entegre olur, böylece API ile ilgili tüm standart winston günlük kaydı kodu zaten yapılır.
  • cors , kaynaklar arası kaynak paylaşımını etkinleştirmemizi sağlayan bir Express.js ara yazılımı parçasıdır. Bu olmadan, API'miz yalnızca arka ucumuzla tam olarak aynı alt etki alanından sunulan ön uçlardan kullanılabilir.

Arka uçumuz çalışırken bu paketleri kullanır. Ancak TypeScript yapılandırmamız için bazı geliştirme bağımlılıklarını da yüklememiz gerekiyor. Bunun için koşacağız:

 npm i --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript

Bu bağımlılıklar, Express.js ve diğer bağımlılıklar tarafından kullanılan türlerle birlikte, uygulamamızın kendi kodu için TypeScript'i etkinleştirmek için gereklidir. Bu, WebStorm veya VSCode gibi bir IDE kullandığımızda, kodlama sırasında bazı işlev yöntemlerini otomatik olarak tamamlamamıza izin vererek çok zaman kazandırabilir.

package.json son bağımlılıklar şöyle olmalıdır:

 "dependencies": { "debug": "^4.2.0", "express": "^4.17.1", "express-winston": "^4.0.5", "winston": "^3.3.3", "cors": "^2.8.5" }, "devDependencies": { "@types/cors": "^2.8.7", "@types/debug": "^4.1.5", "@types/express": "^4.17.2", "source-map-support": "^0.5.16", "tslint": "^6.0.0", "typescript": "^3.7.5" }

Artık gerekli tüm bağımlılıklarımızı yüklediğimize göre, kendi kodumuzu oluşturmaya başlayalım!

TypeScript REST API Proje Yapısı

Bu eğitim için sadece üç dosya oluşturacağız:

  1. ./app.ts
  2. ./common/common.routes.config.ts
  3. ./users/users.routes.config.ts

Proje yapısının iki klasörünün ( common ve users ) arkasındaki fikir, kendi sorumlulukları olan bireysel modüllere sahip olmaktır. Bu anlamda, sonunda her modül için aşağıdakilerden bazılarına veya tümüne sahip olacağız:

  • API'mizin işleyebileceği istekleri tanımlamak için rota yapılandırması
  • Veritabanı modellerimize bağlanma, sorgu yapma veya belirli bir istek için gerekli olan harici hizmetlere bağlanma gibi görevler için hizmetler
  • Bir rotanın son denetleyicisi özelliklerini işlemeden önce belirli istek doğrulamalarını çalıştırmak için ara yazılım
  • Veri depolamayı ve almayı kolaylaştırmak için belirli bir veritabanı şemasıyla eşleşen veri modellerini tanımlamaya yönelik modeller
  • Son olarak (herhangi bir ara katman yazılımından sonra) bir yol isteğini işleyen, gerekirse yukarıdaki hizmet işlevlerini çağıran ve istemciye bir yanıt veren koddan yol yapılandırmasını ayırmak için denetleyiciler

Bu klasör yapısı, temel bir REST API tasarımı, bu eğitim serisinin geri kalanı için erken bir başlangıç ​​noktası ve pratik yapmaya başlamak için yeterli sağlar.

TypeScript'te Ortak Yollar Dosyası

common klasöründe common.routes.config.ts dosyasını aşağıdaki gibi olacak şekilde oluşturalım:

 import express from 'express'; export class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; } getName() { return this.name; } }

Burada rotaları oluşturma şeklimiz isteğe bağlıdır. Ancak extends ile çalıştığımız için, rota senaryomuz, birazdan göreceğimiz gibi, extensions anahtar sözcüğüyle kalıtımı kullanma alıştırması yapma fırsatıdır. Bu projede, tüm rota dosyaları aynı davranışa sahiptir: Bir adları (hata ayıklama amacıyla kullanacağımız) ve ana Express.js Application nesnesine erişimleri vardır.

Artık kullanıcı rota dosyasını oluşturmaya başlayabiliriz. users.routes.config.ts users ve şu şekilde kodlamaya başlayalım:

 import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } }

Burada CommonRoutesConfig sınıfını içe aktarıyoruz ve onu UsersRoutes adlı yeni sınıfımıza genişletiyoruz. Yapıcı ile uygulamayı (ana express.Application nesnesi) ve UsersRoutes adını CommonRoutesConfig 'in yapıcısına göndeririz.

Bu örnek oldukça basittir, ancak birkaç rota dosyası oluşturmak için ölçeklendirirken, bu yinelenen kodu önlememize yardımcı olacaktır.

Bu dosyaya günlük kaydı gibi yeni özellikler eklemek istediğimizi varsayalım. Gerekli alanı CommonRoutesConfig sınıfına ekleyebiliriz ve ardından CommonRoutesConfig genişleten tüm rotalar buna erişebilir.

Sınıflar Arasında Benzer İşlevsellik için TypeScript Soyut İşlevlerini Kullanma

Bu sınıflar arasında benzer bazı işlevlere (API uç noktalarını yapılandırmak gibi) sahip olmak istiyorsak, ancak bu her sınıf için farklı bir uygulamaya ihtiyaç duyarsa ne olur? Seçeneklerden biri, soyutlama adı verilen TypeScript özelliğini kullanmaktır.

UsersRoutes sınıfının (ve gelecekteki yönlendirme sınıflarının) CommonRoutesConfig miras alacağı çok basit bir soyut işlev oluşturalım. Diyelim ki tüm rotaları (böylece onu ortak kurucumuzdan çağırabiliriz) configureRoutes() adında bir fonksiyona sahip olmaya zorlamak istiyoruz. Her yönlendirme sınıfının kaynağının uç noktalarını burada ilan edeceğiz.

Bunu yapmak için common.routes.config.ts üç hızlı şey ekleyeceğiz:

  1. Bu sınıf için abstract etkinleştirmek için class satırımıza özet anahtar sözcüğü.
  2. Sınıfımızın sonunda yeni bir fonksiyon bildirimi, abstract configureRoutes(): express.Application; . Bu, CommonRoutesConfig genişleten herhangi bir sınıfı, bu imzayla eşleşen bir uygulama sağlamaya zorlar; yoksa TypeScript derleyicisi bir hata verir.
  3. this.configureRoutes(); yapıcının sonunda, çünkü artık bu işlevin var olacağından emin olabiliriz.

Sonuç:

 import express from 'express'; export abstract class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; this.configureRoutes(); } getName() { return this.name; } abstract configureRoutes(): express.Application; }

Bununla birlikte, CommonRoutesConfig genişleten herhangi bir sınıf, bir express.Application nesnesi döndüren configureRoutes() adlı bir işleve sahip olmalıdır. Bu, users.routes.config.ts güncellenmesi gerektiği anlamına gelir:

 import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes() { // (we'll add the actual route configuration here next) return this.app; } }

Yaptıklarımızın bir özeti olarak:

Önce common.routes.config dosyasını, ardından express modülünü içe aktarıyoruz. Ardından, CommonRoutesConfig temel sınıfını genişletmesini istediğimizi söyleyerek UserRoutes sınıfını tanımlarız, bu da onun configureRoutes() uygulayacağına söz verdiğimiz anlamına gelir.

CommonRoutesConfig sınıfına bilgi göndermek için sınıfın constructor kullanıyoruz. Bir sonraki adımda daha derinlemesine anlatacağımız express.Application nesnesini almayı bekler. super() ile, CommonRoutesConfig yapıcısına uygulamayı ve bu senaryoda UsersRoutes olan rotalarımızın adını iletiriz. ( super() , sırayla, configureRoutes() uygulamamızı çağırır.)

Kullanıcı Uç Noktalarının Express.js Yollarını Yapılandırma

configureRoutes() işlevi, REST API'mizin kullanıcıları için uç noktaları oluşturacağımız yerdir. Orada, uygulamayı ve rota işlevlerini Express.js'den kullanacağız.

app.route() işlevini kullanma fikri, iyi tanımlanmış kaynaklara sahip bir REST API oluşturduğumuz için kolay olan kod tekrarını önlemektir. Bu eğitim için ana kaynak kullanıcılardır . Bu senaryoda iki durumumuz var:

  • API çağırıcısı yeni bir kullanıcı oluşturmak veya mevcut tüm kullanıcıları listelemek istediğinde, uç noktanın başlangıçta yalnızca istenen yolun sonunda users olması gerekir. (Bu makalede sorgu filtreleme, sayfalandırma veya benzeri sorgulara girmeyeceğiz.)
  • Arayan belirli bir kullanıcı kaydına özel bir şey yapmak istediğinde, isteğin kaynak yolu users/:userId modelini izleyecektir.

.route() 'un Express.js'de çalışma şekli, HTTP fiillerini bazı zarif zincirlemelerle işlememize izin verir. Bunun nedeni, .get( .get() , .post( .post() , vb.'nin tümünün, ilk .route() çağrısının yaptığı aynı IRoute örneğini döndürmesidir. Son yapılandırma şu şekilde olacaktır:

 configureRoutes() { this.app.route(`/users`) .get((req: express.Request, res: express.Response) => { res.status(200).send(`List of users`); }) .post((req: express.Request, res: express.Response) => { res.status(200).send(`Post to users`); }); this.app.route(`/users/:userId`) .all((req: express.Request, res: express.Response, next: express.NextFunction) => { // this middleware function runs before any request to /users/:userId // but it doesn't accomplish anything just yet--- // it simply passes control to the next applicable function below using next() next(); }) .get((req: express.Request, res: express.Response) => { res.status(200).send(`GET requested for id ${req.params.userId}`); }) .put((req: express.Request, res: express.Response) => { res.status(200).send(`PUT requested for id ${req.params.userId}`); }) .patch((req: express.Request, res: express.Response) => { res.status(200).send(`PATCH requested for id ${req.params.userId}`); }) .delete((req: express.Request, res: express.Response) => { res.status(200).send(`DELETE requested for id ${req.params.userId}`); }); return this.app; }

Yukarıdaki kod, herhangi bir REST API istemcisinin, users bir POST veya GET isteği ile uç noktalarını aramasına izin verir. Benzer şekilde, bir müşterinin /users/:userId uç noktamızı bir GET , PUT , PATCH veya DELETE isteği ile çağırmasına izin verir.

Ancak /users/:userId için, all() işlevini kullanarak, get() , put() , patch() veya delete() işlevlerinden herhangi birinden önce çalıştırılacak olan genel ara katman yazılımını da ekledik. Bu işlev (seride daha sonra) yalnızca kimliği doğrulanmış kullanıcılar tarafından erişilmesi amaçlanan yollar oluşturduğumuzda faydalı olacaktır.

Herhangi bir ara katman yazılımında olduğu gibi .all() işlevimizde de üç tür alanımız olduğunu fark etmiş olabilirsiniz: Request , Response ve NextFunction .

  • İstek, Express.js'nin işlenecek HTTP isteğini temsil etme şeklidir. Bu tür, yerel Node.js istek türünü yükseltir ve genişletir.
  • Yanıt aynı şekilde Express.js'nin HTTP yanıtını temsil etme şeklidir ve yine yerel Node.js yanıt türünü genişletir.
  • Daha az önemli NextFunction , kontrolün diğer ara yazılım işlevlerinden geçmesine izin veren bir geri arama işlevi olarak hizmet eder. Yol boyunca, denetleyici nihayet istekte bulunana bir yanıt göndermeden önce tüm ara katman yazılımı aynı istek ve yanıt nesnelerini paylaşacaktır.

Node.js Giriş Noktası Dosyamız, app.ts

Artık bazı temel rota iskeletlerini yapılandırdığımıza göre, uygulamanın giriş noktasını yapılandırmaya başlayacağız. Proje klasörümüzün kökünde app.ts dosyasını oluşturalım ve bu kodla başlayalım:

 import express from 'express'; import * as http from 'http'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; import cors from 'cors'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import debug from 'debug';

Makalenin bu noktasında bu ithalatlardan yalnızca ikisi yenidir:

  • http , bir Node.js-native modülüdür. Express.js uygulamamızı başlatmamız gerekiyor.
  • body-parser , Express.js ile birlikte gelen ara katman yazılımıdır. Kontrol kendi istek işleyicilerimize gitmeden önce isteği (bizim durumumuzda JSON olarak) ayrıştırır.

Artık dosyaları içe aktardığımıza göre, kullanmak istediğimiz değişkenleri bildirmeye başlayacağız:

 const app: express.Application = express(); const server: http.Server = http.createServer(app); const port = 3000; const routes: Array<CommonRoutesConfig> = []; const debugLog: debug.IDebugger = debug('app');

express() işlevi, http.Server nesnesine ekleyerek başlayarak kodumuz boyunca aktaracağımız ana Express.js uygulama nesnesini döndürür. (express.Application'ımızı yapılandırdıktan sonra http.Server başlatmamız express.Application .)

Standart bağlantı noktaları 80 (HTTP) veya 443 (HTTPS) yerine TypeScript'in otomatik olarak bir Number olduğunu çıkaracağı 3000 numaralı bağlantı noktasını dinleyeceğiz, çünkü bunlar genellikle bir uygulamanın ön ucu için kullanılacaktır.

Neden Port 3000?

Bağlantı noktasının 3000 olması gerektiğine dair bir kural yoktur - belirtilmemişse isteğe bağlı bir bağlantı noktası atanır - ancak hem Node.js hem de Express.js için belgelendirme örnekleri boyunca 3000 kullanılır, bu nedenle geleneği burada sürdürüyoruz.

Node.js Bağlantı Noktalarını Ön Uçla Paylaşabilir mi?

Arka uçumuzun standart bağlantı noktalarındaki isteklere yanıt vermesini istediğimizde bile, özel bir bağlantı noktasında yerel olarak çalışabiliriz. Bu, belirli bir etki alanı veya alt etki alanı ile 80 veya 443 numaralı bağlantı noktasındaki istekleri almak için bir ters proxy gerektirir. Daha sonra onları dahili bağlantı noktamız 3000'e yönlendirir.

routes dizisi, aşağıda göreceğimiz gibi, hata ayıklama amacıyla rota dosyalarımızı takip edecektir.

Son olarak, debugLog , console.log benzer bir işlev olarak sona erecektir, ancak daha iyisi: Dosya/modül bağlamımızı çağırmak istediğimiz her şeye otomatik olarak dahil edildiğinden, ince ayar yapmak daha kolaydır. (Bu durumda, bunu bir dizgede debug() yapıcısına ilettiğimizde buna “app” adını verdik.)

Artık tüm Express.js ara katman yazılım modüllerimizi ve API'mizin rotalarını yapılandırmaya hazırız:

 // here we are adding middleware to parse all incoming requests as JSON app.use(express.json()); // here we are adding middleware to allow cross-origin requests app.use(cors()); // here we are preparing the expressWinston logging middleware configuration, // which will automatically log all HTTP requests handled by Express.js const loggerOptions: expressWinston.LoggerOptions = { transports: [new winston.transports.Console()], format: winston.format.combine( winston.format.json(), winston.format.prettyPrint(), winston.format.colorize({ all: true }) ), }; if (!process.env.DEBUG) { loggerOptions.meta = false; // when not debugging, log requests as one-liners } // initialize the logger with the above configuration app.use(expressWinston.logger(loggerOptions)); // here we are adding the UserRoutes to our array, // after sending the Express.js application object to have the routes added to our app! routes.push(new UsersRoutes(app)); // this is a simple route to make sure everything is working properly const runningMessage = `Server running at http://localhost:${port}`; app.get('/', (req: express.Request, res: express.Response) => { res.status(200).send(runningMessage) });

expressWinston.logger , Express.js'ye bağlanır ve tamamlanan her istek için ayrıntıları debug aynı altyapı aracılığıyla otomatik olarak günlüğe kaydeder. Ona ilettiğimiz seçenekler, hata ayıklama modundayken daha ayrıntılı günlük kaydı (varsayılan) ile karşılık gelen terminal çıktısını düzgün bir şekilde biçimlendirecek ve renklendirecektir.

expressWinston.logger sonra rotalarımızı tanımlamamız gerektiğini unutmayın.

Son olarak ve en önemlisi:

 server.listen(port, () => { routes.forEach((route: CommonRoutesConfig) => { debugLog(`Routes configured for ${route.getName()}`); }); // our only exception to avoiding console.log(), because we // always want to know when the server is done starting up console.log(runningMessage); });

Bu aslında sunucumuzu başlatır. Başladıktan sonra, Node.js, hata ayıklama modunda yapılandırdığımız tüm yolların adlarını bildiren geri çağırma işlevimizi çalıştıracaktır - şu ana kadar yalnızca UsersRoutes . Bundan sonra, geri aramamız, üretim modunda çalışırken bile arka ucumuzun istekleri almaya hazır olduğunu bize bildirir.

TypeScript'i JavaScript'e Aktarmak ve Uygulamayı Çalıştırmak için package.json güncelleme

Artık iskeletimizi çalıştırmaya hazır hale getirdiğimize göre, önce TypeScript aktarımını etkinleştirmek için bazı standart yapılandırma yapılandırmasına ihtiyacımız var. Proje köküne tsconfig.json dosyasını ekleyelim:

 { "compilerOptions": { "target": "es2016", "module": "commonjs", "outDir": "./dist", "strict": true, "esModuleInterop": true, "inlineSourceMap": true } }

Ardından, package.json aşağıdaki komut dosyaları biçiminde son dokunuşları eklememiz yeterli:

 "scripts": { "start": "tsc && node --unhandled-rejections=strict ./dist/app.js", "debug": "export DEBUG=* && npm run start", "test": "echo \"Error: no test specified\" && exit 1" },

test komut dosyası, seride daha sonra değiştireceğimiz bir yer tutucudur.

start ​​betiğindeki tsc , TypeScript'e aittir. TypeScript kodumuzu, dist klasörüne çıkaracağı JavaScript'e aktarmaktan sorumludur. Ardından, yerleşik sürümü node ./dist/app.js ile çalıştırıyoruz.

--unhandled-rejections=strict iletiyoruz (Node.js v16+ ile bile) çünkü pratikte, düz bir "çarpma ve yığını göster" yaklaşımını kullanarak hata ayıklama, bir expressWinston.errorLogger nesnesi ile daha gösterişli günlük kaydı yapmaktan daha basittir. Bu, çoğunlukla, işlenmeyen bir reddetmeye rağmen Node.js'nin çalışmaya devam etmesine izin verilmesinin sunucuyu beklenmedik bir durumda bırakarak daha fazla (ve daha karmaşık) hataların oluşmasına izin verdiği üretimde bile geçerlidir.

debug komut dosyası, start komut dosyasını çağırır, ancak önce bir DEBUG ortam değişkeni tanımlar. Bu, tüm debugLog() deyimlerimizin (artı bizim yaptığımız debug modülünü kullanan Express.js'nin kendisinden benzerleri) terminale yararlı ayrıntıların çıktısını almak için etkinleştirme etkisine sahiptir; aksi halde (uygun bir şekilde) çalıştırılırken gizlenen ayrıntılar sunucu standart bir npm start ile üretim modunda.

npm run debug kendiniz çalıştırmayı deneyin ve ardından konsol çıktısının nasıl değiştiğini görmek için bunu npm start ile karşılaştırın.

İpucu: DEBUG=* yerine DEBUG=app kullanarak hata ayıklama çıktısını app.ts dosyamızın kendi debugLog() ifadeleriyle sınırlayabilirsiniz. debug modülü genellikle oldukça esnektir ve bu özellik bir istisna değildir.

Dışa export Mac ve Linux'ta çalışma şekli bu olduğundan, Windows kullanıcılarının muhtemelen export SET olarak değiştirmeleri gerekecektir. Projenizin birden çok geliştirme ortamını desteklemesi gerekiyorsa, çapraz ortam paketi burada basit bir çözüm sunar.

Live Express.js Arka Uçunu Test Etme

npm run debug veya npm start devam ederken, REST API'miz 3000 numaralı bağlantı noktasındaki isteklere hizmet vermeye hazır olacaktır. Bu noktada, arka ucu test etmek için cURL, Postman, Insomnia vb. kullanabiliriz.

Kullanıcı kaynağı için yalnızca bir iskelet oluşturduğumuz için, her şeyin beklendiği gibi çalıştığını görmek için bir gövde olmadan istek gönderebiliriz. Örneğin:

 curl --request GET 'localhost:3000/users/12345'

Arka GET requested for id 12345 yanıtını geri göndermelidir.

POST ing'e gelince:

 curl --request POST 'localhost:3000/users' \ --data-raw ''

Bu ve iskeletler oluşturduğumuz diğer tüm istek türleri oldukça benzer görünecek.

TypeScript ile Rapid Node.js REST API Geliştirmeye Hazır

Bu yazıda, projeyi sıfırdan yapılandırarak ve Express.js çerçevesinin temellerine dalarak bir REST API oluşturmaya başladık. Ardından, bu serideki bir sonraki makalede yeniden kullanacağımız UsersRoutesConfig genişleten CommonRoutesConfig ile bir model oluşturarak TypeScript'te uzmanlaşmaya yönelik ilk adımımızı attık. Uygulamamızı oluşturmak ve çalıştırmak için app.ts giriş noktamızı yeni rotalarımızı ve package.json komut dosyalarıyla kullanacak şekilde yapılandırarak bitirdik.

Ancak Express.js ve TypeScript ile yapılan bir REST API'sinin temelleri bile oldukça ilgili. Bu dizinin bir sonraki bölümünde, kullanıcı kaynağı için uygun denetleyiciler oluşturmaya odaklanacağız ve hizmetler, ara katman yazılımı, denetleyiciler ve modeller için bazı yararlı kalıpları inceleyeceğiz.

Projenin tamamı GitHub'da mevcuttur ve bu makalenin sonundaki kod toptal-article-01 dalında bulunur.