Node.js/TypeScript REST API Oluşturma, Bölüm 2: Modeller, Ara Yazılımlar ve Hizmetler
Yayınlanan: 2022-03-11REST API serimizin ilk makalesinde, sıfırdan bir arka uç oluşturmak, TypeScript gibi bağımlılıklar eklemek, Node.js'de yerleşik debug modülünü kullanmak, bir Express.js proje yapısı oluşturmak ve çalışma zamanını günlüğe kaydetmek için npm'nin nasıl kullanılacağını ele aldık. Winston ile esnek bir şekilde olaylar. Bu kavramlardan zaten memnunsanız, bunu klonlayın, git checkout ile toptal-article-01 şubesine geçin ve okumaya devam edin.
REST API Hizmetleri, Ara Yazılım, Denetleyiciler ve Modeller
Söz verdiğimiz gibi, şimdi bu modüller hakkında ayrıntılara gireceğiz:
- İş mantığı işlemlerini, ara katman yazılımının ve denetleyicilerin çağırabileceği işlevlere dahil ederek kodumuzu daha temiz hale getiren hizmetler .
- Express.js uygun denetleyici işlevini çağırmadan önce önkoşul koşullarını doğrulayacak ara yazılım.
- Son olarak istek sahibine bir yanıt göndermeden önce isteği işlemek için hizmetleri kullanan denetleyiciler .
- Verilerimizi tanımlayan ve derleme zamanı kontrollerine yardımcı olan modeller .
Ayrıca üretim için hiçbir şekilde uygun olmayan çok temel bir veritabanı ekleyeceğiz. (Tek amacı, bu öğreticiyi takip etmeyi kolaylaştırmak ve bir sonraki makalemizin veritabanı bağlantısı ve MongoDB ve Mongoose ile entegrasyona girmesinin önünü açmaktır.)
Uygulamalı: DAO'lar, DTO'lar ve Geçici Veritabanımızla İlk Adımlar
Eğitimimizin bu kısmı için veritabanımız dosyaları bile kullanmayacak. Kullanıcı verilerini bir dizide tutacaktır; bu, Node.js'den her çıktığımızda verilerin buharlaştığı anlamına gelir. Yalnızca en temel oluşturma, okuma, güncelleme ve silme (CRUD) işlemlerini destekleyecektir.
Burada iki kavram kullanacağız:
- Veri erişim nesneleri (DAO'lar)
- Veri aktarım nesneleri (DTO'lar)
Kısaltmalar arasındaki bu tek harfli fark çok önemlidir: Bir DAO, tanımlanmış bir veritabanına bağlanmaktan ve CRUD işlemlerini gerçekleştirmekten sorumludur; DTO, DAO'nun veritabanına göndereceği ve veritabanından alacağı ham verileri tutan bir nesnedir.
Başka bir deyişle, DTO'lar veri modeli türlerine uyan nesnelerdir ve DAO'lar bunları kullanan hizmetlerdir.
DTO'lar, örneğin bu makalede iç içe geçmiş veritabanı varlıklarını temsil ederek daha karmaşık hale gelebilse de, tek bir DTO örneği, tek bir veritabanı satırındaki belirli bir eyleme karşılık gelir.
Neden DTO'lar?
TypeScript nesnelerimizin veri modellerimize uyması için DTO'ların kullanılması, aşağıdaki hizmetler bölümünde göreceğimiz gibi, mimari tutarlılığın korunmasına yardımcı olur. Ancak çok önemli bir uyarı var: Ne DTO'lar ne de TypeScript'in kendisi, çalışma zamanında gerçekleşmesi gerekeceğinden, herhangi bir otomatik kullanıcı girişi doğrulaması vaat etmiyor. Kodumuz, API'mizdeki bir uç noktada kullanıcı girdisi aldığında, bu girdi şunları yapabilir:
- Fazladan alanları var
- Zorunlu alanların eksik olması (yani, son eki
?) - TypeScript kullanarak modelimizde belirttiğimiz türde veri olmayan alanlara sahip olun
TypeScript (ve aktarıldığı JavaScript) bunu bizim için sihirli bir şekilde kontrol etmeyecektir, bu nedenle özellikle API'nizi halka açarken bu doğrulamaları unutmamak önemlidir. ajv gibi paketler bu konuda yardımcı olabilir, ancak normalde modelleri yerel TypeScript yerine kitaplığa özgü şema nesnesinde tanımlayarak çalışır. (Bir sonraki makalede tartışılan Mongoose, bu projede benzer bir rol oynayacak.)
"Daha basit bir şey yerine hem DAO'ları hem de DTO'ları kullanmak gerçekten en iyisi mi?" diye düşünüyor olabilirsiniz. Kurumsal geliştirici Gunther Popp bir cevap sunuyor; orta vadede ölçeklendirmeyi makul bir şekilde bekleyemiyorsanız, gerçek dünyadaki çoğu Express.js/TypeScript projesinde DTO'lardan kaçınmak isteyeceksiniz.
Ancak bunları üretimde kullanmak üzere olmasanız bile, bu örnek proje, TypeScript API mimarisine hakim olma yolunda değerli bir fırsattır. TypeScript türlerini ek yollarla kullanma alıştırması yapmak ve bileşen ve modeller eklerken daha temel bir yaklaşımla nasıl karşılaştırıldıklarını görmek için DTO'larla çalışmak için harika bir yoldur.
TypeScript Düzeyinde Kullanıcı REST API Modelimiz
İlk önce kullanıcımız için üç DTO tanımlayacağız. create.user.dto.ts dto users içeren bir dosya oluşturalım:
export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }Veritabanından bağımsız olarak her kullanıcı oluşturduğumuzda, bir kimliği, şifresi ve e-postası ve isteğe bağlı olarak bir adı ve soyadı olması gerektiğini söylüyoruz. Bu gereksinimler, belirli bir projenin iş gereksinimlerine göre değişebilir.
PUT istekleri için tüm nesneyi güncellemek istiyoruz, bu nedenle isteğe bağlı alanlarımız artık gerekli. Aynı klasörde, bu kodla put.user.dto.ts adlı bir dosya oluşturun:
export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } PATCH istekleri için TypeScript'ten, başka bir türü kopyalayarak ve tüm alanlarını isteğe bağlı hale getirerek yeni bir tür oluşturan Partial özelliğini kullanabiliriz. Bu şekilde, patch.user.dto.ts dosyasının yalnızca aşağıdaki kodu içermesi gerekir:
import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} Şimdi in-memory geçici veritabanını oluşturalım. Users klasörünün içinde daos adında bir klasör oluşturalım ve users adında bir dosya users.dao.ts .
İlk olarak, oluşturduğumuz DTO'ları içe aktarmak istiyoruz:
import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';Şimdi, kullanıcı kimliklerimizi işlemek için kısa kitaplığı ekleyelim (terminali kullanarak):
npm i shortid npm i --save-dev @types/shortid users.dao.ts geri döndüğümüzde, kısayolu içe aktaracağız:
import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); Artık şu şekilde görünecek olan UsersDao adında bir sınıf oluşturabiliriz:
class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); Singleton modelini kullanarak, bu sınıf, diğer dosyalara içe aktardığımızda her zaman aynı örneği ve kritik olarak aynı users dizisini sağlayacaktır. Bunun nedeni, Node.js'nin bu dosyayı içe aktarıldığı her yerde önbelleğe alması ve tüm içe aktarmaların başlangıçta gerçekleşmesidir. Yani, users.dao.ts atıfta bulunan herhangi bir dosyaya, Node.js bu dosyayı ilk kez işlediğinde dışa aktarılan aynı new UsersDao() ya bir referans verilecektir.
Bu makalenin ilerleyen bölümlerinde bu sınıfı kullandığımızda ve proje boyunca çoğu sınıf için bu ortak TypeScript/Express.js modelini kullandığımızda bunun işe yaradığını göreceğiz.
Not: Singleton'ların sıkça bahsedilen bir dezavantajı, birim testleri yazmanın zor olmasıdır. Sınıflarımızın çoğu durumunda, sıfırlanması gereken herhangi bir sınıf üyesi değişkeni olmadığından bu dezavantaj geçerli olmayacaktır. Ancak olması gerekenler için, okuyucunun bu soruna bağımlılık enjeksiyonu kullanımıyla yaklaşmayı düşünmesi için bir alıştırma olarak bırakıyoruz.
Şimdi temel CRUD işlemlerini fonksiyon olarak sınıfa ekleyeceğiz. Oluşturma işlevi şöyle görünecektir:
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }Okuma , "tüm kaynakları oku" ve "kimliğe göre oku" olmak üzere iki farklı şekilde gelir. Şu şekilde kodlanmışlar:
async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } Benzer şekilde, güncelleme , nesnenin tamamının ( PUT olarak) veya nesnenin yalnızca bölümlerinin ( PATCH olarak) üzerine yazılması anlamına gelir:
async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; } Daha önce belirtildiği gibi, bu işlev imzalarındaki UserDto bildirimimize rağmen TypeScript, çalışma zamanı türü denetimi sağlamaz. Bu şu demek:
-
putUserById()bir hata içeriyor. API tüketicilerinin, DTO'muz tarafından tanımlanan modelin parçası olmayan alanlar için değerleri depolamasına izin verecektir. -
patchUserById(), modelle senkronize tutulması gereken alan adlarının yinelenen listesine bağlıdır. Bu olmadan, bu liste için güncellenen nesneyi kullanmak zorunda kalacaktı. Bu, DTO tanımlı modelin parçası olan ancak bu belirli nesne örneği için daha önce kaydedilmemiş alanların değerlerini sessizce yok sayacağı anlamına gelir.
Ancak bu senaryoların her ikisi de bir sonraki makalede veritabanı düzeyinde doğru bir şekilde ele alınacaktır.
Bir kaynağı silmek için son işlem şöyle görünecektir:
async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }Bonus olarak, bir kullanıcı oluşturmanın ön koşulunun, kullanıcı e-postasının yinelenmediğini doğrulamak olduğunu bilerek, şimdi bir “kullanıcıyı e-posta ile al” işlevi ekleyelim:
async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } }Not: Gerçek dünya senaryosunda, muhtemelen ihtiyacınız olabilecek tüm temel işlemleri özetleyecek olan Mongoose veya Sequelize gibi önceden var olan bir kitaplığı kullanarak bir veritabanına bağlanacaksınız. Bu nedenle, yukarıda uygulanan işlevlerin ayrıntılarına girmiyoruz.
REST API Hizmetleri Katmanımız
Artık temel bir bellek içi DAO'muz olduğuna göre, CRUD işlevlerini çağıracak bir hizmet oluşturabiliriz. CRUD işlevleri, bir veritabanına bağlanacak her hizmetin sahip olması gereken bir şey olduğundan, her yeni bir hizmeti uygulamak istediğimizde uygulamak istediğimiz yöntemleri içeren bir CRUD arabirimi oluşturacağız.
Günümüzde, birlikte çalıştığımız IDE'ler, uyguladığımız işlevleri eklemek için kod oluşturma özelliklerine sahip olup, yazmamız gereken tekrarlayan kod miktarını azaltmaktadır.
WebStorm IDE'yi kullanan hızlı bir örnek:
IDE, MyService sınıf adını vurgular ve aşağıdaki seçenekleri önerir:
"Tüm üyeleri uygula" seçeneği, CRUD arayüzüne uymak için gereken işlevleri anında oluşturur:
Tüm bunlar, önce CRUD adlı TypeScript arabirimimizi oluşturalım. common klasörümüzde interfaces adında bir klasör oluşturalım ve aşağıdakilerle crud.interface.ts :
export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; } Bunu yaptıktan sonra, users klasörü içinde bir services klasörü oluşturalım ve aşağıdakileri içeren users.service.ts dosyasını ekleyelim:
import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService(); Buradaki ilk adımımız, bellek içi DAO'muzu, arayüz bağımlılığımızı ve DTO'larımızın her birinin TypeScript türünü içe aktarmaktı, DAO'muzda kullandığımız aynı model olan UsersService bir hizmet singletonu olarak uygulama zamanı.
Tüm CRUD işlevleri artık yalnızca UsersDao ilgili işlevlerini çağırıyor. DAO'yu değiştirme zamanı geldiğinde, Bölüm 3'te göreceğimiz gibi, DAO işlevlerinin çağrıldığı bu dosyaya yapılan bazı ince ayarlar dışında, projede başka hiçbir yerde değişiklik yapmak zorunda kalmayacağız.
Örneğin, list() 'e yapılan her çağrıyı izlememiz ve değiştirmeden önce bağlamını kontrol etmemiz gerekmeyecek. Yukarıda gördüğünüz küçük miktarda ilk kazan plakası pahasına bu ayırma katmanına sahip olmanın avantajı budur.
Async/Await ve Node.js
Hizmet işlevleri için zaman async kullanmamız anlamsız görünebilir. Şimdilik böyle: Tüm bu işlevler, Promise s veya await herhangi bir dahili kullanımı olmadan hemen değerlerini döndürür. Bu, yalnızca kod tabanımızı async kullanacak hizmetlere hazırlamak içindir. Benzer şekilde, aşağıda, bu işlevlere yapılan tüm çağrıların wait kullandığını await .
Bu makalenin sonunda, deneyebileceğiniz tekrar çalıştırılabilir bir projeniz olacak. Bu, kod tabanındaki farklı yerlere çeşitli türde hatalar eklemeyi denemek ve derleme ve test sırasında neler olduğunu görmek için harika bir an olacaktır. Özellikle zaman async bir bağlamdaki hatalar, beklediğiniz gibi davranmayabilir. Bu makalenin kapsamı dışında kalan çeşitli çözümleri araştırmaya ve araştırmaya değer.

Şimdi DAO ve servislerimizi hazır hale getirdikten sonra kullanıcı kontrolörüne geri dönelim.
REST API Denetleyicimizi Oluşturma
Yukarıda söylediğimiz gibi, denetleyicilerin arkasındaki fikir, rota yapılandırmasını, sonunda bir rota isteğini işleyen koddan ayırmaktır. Bu, talebimiz denetleyiciye ulaşmadan önce tüm doğrulamaların yapılması gerektiği anlamına gelir. Denetleyicinin yalnızca gerçek istekle ne yapacağını bilmesi gerekir, çünkü istek o kadar ileri gittiyse, geçerli olduğunu biliriz. Kontrolör daha sonra ele alacağı her talebin ilgili servisini arayacaktır.
Başlamadan önce, kullanıcı parolasını güvenli bir şekilde karma hale getirmek için bir kitaplık kurmamız gerekecek:
npm i argon2 users denetleyici klasörünün içinde controllers adlı bir klasör oluşturarak ve bunun içinde users.controller.ts adlı bir dosya oluşturarak başlayalım:
// we import express to add types to the request/response objects from our controller functions import express from 'express'; // we import our newly created user services import usersService from '../services/users.service'; // we import the argon2 library for password hashing import argon2 from 'argon2'; // we use debug with a custom context as described in Part 1 import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController(); Not: HTTP 204 No Content yanıtıyla hiçbir şey geri göndermeyen yukarıdaki satırlar, konuyla ilgili RFC 7231 ile uyumludur.
Kullanıcı denetleyicisi singleton'umuz bittiğinde, örnek REST API nesne modelimize ve hizmetimize bağlı olan diğer modülü kodlamaya hazırız: kullanıcı ara katman yazılımımız.
Express.js ile Node.js REST Ara Yazılımı
Express.js ara yazılımıyla ne yapabiliriz? Doğrulamalar, biri için harika bir seçimdir. İstekler, kullanıcı denetleyicimize ulaşmadan önce ağ geçidi bekçileri olarak hareket etmek için bazı temel doğrulamaları ekleyelim:
- Bir kullanıcı oluşturmak veya güncellemek için gerektiği şekilde
emailvepasswordgibi kullanıcı alanlarının bulunmasını sağlayın - Belirli bir e-postanın halihazırda kullanımda olmadığından emin olun
- Oluşturduktan sonra
emailalanını değiştirmediğimizden emin olun (çünkü bunu basitlik için kullanıcıya yönelik birincil kimlik olarak kullanıyoruz) - Belirli bir kullanıcının var olup olmadığını doğrulama
Bu doğrulamaların Express.js ile çalışmasını sağlamak için, önceki makalede anlatıldığı gibi next() kullanarak Express.js akış denetimi modelini izleyen işlevlere çevirmemiz gerekecek. Yeni bir dosyaya ihtiyacımız olacak, users/middleware/users.middleware.ts :
import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware();Tanıdık singleton kazan plakası ortadan kaldırıldığında, sınıf gövdesine ara katman yazılımı işlevlerimizden bazılarını ekleyelim:
async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: `Missing required fields email and password`, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: `User email already exists` }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: `Invalid email` }); } } // Here we need to use an arrow function to bind `this` correctly validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: `User ${req.params.userId} not found`, }); } } API tüketicilerimizin yeni eklenen bir kullanıcı hakkında daha fazla istekte bulunmasını kolaylaştırmak için, istek URL'sinin kendisinden gelen kullanıcı userId istek parametrelerinden çıkaracak ve onu ekleyecek bir yardımcı işlev ekleyeceğiz. kullanıcı verilerinin geri kalanının bulunduğu istek gövdesi.
Buradaki fikir, kullanıcı bilgilerini güncellemek istediğimizde, her seferinde parametrelerden kimlik alma endişesi olmadan tam gövde talebini basitçe kullanabilmektir. Bunun yerine, tek bir noktada, ara katman yazılımında halledilir. İşlev şöyle görünecek:
async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } Mantığın yanı sıra, ara katman yazılımı ile denetleyici arasındaki temel fark, şimdi, bizim durumumuzda denetleyici olan nihai hedefe ulaşana kadar bir yapılandırılmış işlevler zinciri boyunca denetimi geçirmek için next() işlevini kullanmamızdır.
Hepsini Bir Araya Getirmek: Rotalarımızı Yeniden Düzenlemek
Artık proje mimarimizin tüm yeni yönlerini uyguladığımıza göre, bir önceki makalede tanımladığımız users.routes.config.ts dosyasına geri dönelim. Her ikisi de kullanıcı hizmetimize dayanan ve sırayla kullanıcı modelimizi gerektiren ara katman yazılımımızı ve denetleyicilerimizi arayacaktır.
Son dosya bu kadar basit olacaktır:
import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } } Burada, iş mantığımızı doğrulamak için ara katman yazılımı ve her şey geçerliyse talebi işlemek için uygun denetleyici işlevleri ekleyerek rotalarımızı yeniden tanımladık. Ayrıca, userId öğesini çıkarmak için Express.js'den .param() işlevini kullandık.
.all() işlevinde, herhangi bir GET , PUT , PATCH veya DELETE /users/:userId bitiş noktasından geçmeden önce çağrılacak şekilde UsersMiddleware validateUserExists işlevimizi iletiyoruz. Bu, validateUserExists .put() veya .patch() ilettiğimiz ek işlev dizilerinde bulunması gerekmediği anlamına gelir; burada belirtilen işlevlerden önce çağrılır.
Ara katman yazılımının doğasında bulunan yeniden kullanılabilirliğinden burada başka bir şekilde de yararlandık. Hem POST hem de PUT bağlamlarında kullanılacak UsersMiddleware.validateRequiredUserBodyFields ileterek, onu diğer ara katman yazılımı işlevleriyle zarif bir şekilde yeniden birleştiriyoruz.
Sorumluluk Reddi: Bu makalede yalnızca temel doğrulamaları ele alıyoruz. Gerçek dünyadaki bir projede, kodlamanız gereken tüm kısıtlamaları düşünmeniz ve bulmanız gerekecektir. Basitlik adına, bir kullanıcının e-postasını değiştiremeyeceğini de varsayıyoruz.
Express/TypeScript REST API'mizi Test Etme
Artık Node.js uygulamamızı derleyip çalıştırabiliriz. Çalıştığında, Postman veya cURL gibi bir REST istemcisi kullanarak API rotalarımızı test etmeye hazırız.
Önce kullanıcılarımızı almaya çalışalım:
curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'Bu noktada, yanıt olarak doğru olan boş bir dizimiz olacak. Şimdi bununla ilk kullanıcı kaynağını oluşturmaya çalışabiliriz:
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'Node.js uygulamamızın artık ara katman yazılımımızdan bir hata göndereceğini unutmayın:
{ "error": "Missing required fields email and password" } Bunu düzeltmek için /users kaynağına göndermek için geçerli bir istek gönderelim:
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'Bu sefer aşağıdaki gibi bir şey görmeliyiz:
{ "id": "ksVnfnPVW" } Bu id , yeni oluşturulan kullanıcının tanımlayıcısıdır ve makinenizde farklı olacaktır. Kalan test ifadelerini kolaylaştırmak için, aldığınız komutla bu komutu çalıştırabilirsiniz (Linux benzeri bir ortam kullandığınızı varsayarak):
REST_API_EXAMPLE_ Şimdi yukarıdaki değişkeni kullanarak bir GET isteği yapmaktan aldığımız yanıtı görebiliriz:
curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' Artık tüm kaynağı aşağıdaki PUT isteğiyle de güncelleyebiliriz:
curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }'Ayrıca e-posta adresini değiştirerek doğrulamamızın çalışıp çalışmadığını test edebiliriz, bu da bir hataya neden olur.
Bir kaynak kimliğine PUT kullanırken, API tüketicileri olarak standart REST modeline uymak istiyorsak tüm nesneyi göndermemiz gerektiğini unutmayın. Bu, yalnızca lastName alanını güncellemek istiyorsak, ancak PUT uç noktamızı kullanarak, güncellenecek tüm nesneyi göndermeye zorlanacağımız anlamına gelir. Yalnızca lastName alanını göndermek hala standart REST kısıtlamaları dahilinde olduğundan, bir PATCH isteğini kullanmak daha kolay olurdu:
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' Kendi kod tabanımızda, bu makalede eklediğimiz ara yazılım işlevlerini kullanarak PUT ve PATCH arasındaki bu ayrımı zorlayan rota yapılandırmamız olduğunu hatırlayın.
PUT , PATCH veya Her İkisi mi?
PATCH esnekliği göz önüne alındığında PUT desteklemek için fazla bir neden yokmuş gibi görünebilir ve bazı API'ler bu yaklaşımı benimser. Diğerleri, API'yi "tamamen REST uyumlu" hale getirmek için PUT desteklemekte ısrar edebilir; bu durumda, alan başına PUT yolları oluşturmak, yaygın kullanım durumları için uygun bir taktik olabilir.
Gerçekte, bu noktalar, ikisi arasındaki gerçek yaşam farklılıklarından yalnızca PATCH için daha esnek anlambilime kadar uzanan çok daha büyük bir tartışmanın parçasıdır. Burada PUT desteğini ve basitlik için yaygın olarak kullanılan PATCH semantiğini sunuyoruz, ancak okuyucuları kendilerini hazır hissettiklerinde daha fazla araştırma yapmaya teşvik ediyoruz.
Kullanıcı listesini yukarıda yaptığımız gibi tekrar aldığımızda, oluşturduğumuz kullanıcımızı alanları güncellenmiş olarak görmeliyiz:
[ { "id": "ksVnfnPVW", "email": "[email protected]", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ]Son olarak, kullanıcıyı silmeyi şu şekilde test edebiliriz:
curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'Kullanıcı listesini tekrar aldığımızda, silinen kullanıcının artık mevcut olmadığını görmeliyiz.
Bununla, users kaynak çalışması için tüm CRUD işlemlerine sahibiz.
Node.js/TypeScript REST API'si
Serinin bu bölümünde, Express.js kullanarak bir REST API oluşturmanın temel adımlarını daha ayrıntılı inceledik. Kodumuzu destek hizmetleri, ara katman yazılımı, denetleyiciler ve modeller için ayırdık. Doğrulama, mantıksal işlemler veya geçerli istekleri işleme ve bunlara yanıt verme gibi işlevlerin her birinin belirli bir rolü vardır.
Ayrıca, bu noktada bazı testlere izin vermek ve ardından serimizin bir sonraki bölümünde daha pratik bir şeyle değiştirilmek amacıyla (punto pardon) açık amacı ile verileri depolamak için çok basit bir yol yarattık.
Basitliği göz önünde bulundurarak bir API oluşturmanın yanı sıra, örneğin tekli sınıfları kullanarak, bakımı daha kolay, daha ölçeklenebilir ve güvenli hale getirmek için atılması gereken birkaç adım vardır. Serinin son makalesinde şunları ele alıyoruz:
- Bellek içi veritabanını MongoDB ile değiştirmek, ardından kodlama sürecini basitleştirmek için Mongoose kullanmak
- JWT ile durum bilgisi olmayan bir yaklaşımda bir güvenlik katmanı ekleme ve erişimi kontrol etme
- Uygulamamızın ölçeklenmesine izin vermek için otomatik testi yapılandırma
Bu makaledeki son koda buradan göz atabilirsiniz.
