Crearea unui API REST Node.js/TypeScript, Partea 2: Modele, Middleware și Servicii
Publicat: 2022-03-11În primul articol din seria noastră de API REST, am descris cum să folosim npm pentru a crea un back-end de la zero, a adăuga dependențe precum TypeScript, a folosi modulul de debug încorporat în Node.js, a construi o structură de proiect Express.js și a înregistra timpul de execuție. evenimentele în mod flexibil cu Winston. Dacă deja sunteți confortabil cu aceste concepte, pur și simplu clonați acest lucru, treceți la toptal-article-01 cu git checkout și citiți mai departe.
Servicii API REST, middleware, controlere și modele
După cum am promis, vom intra acum în detalii despre aceste module:
- Servicii care fac codul nostru mai curat prin încapsularea operațiunilor logice de afaceri în funcții pe care middleware-ul și controlorii le pot apela.
- Middleware care va valida condițiile prealabile înainte ca Express.js să apeleze funcția de controler corespunzătoare.
- Controlorii care utilizează servicii pentru a procesa cererea înainte de a trimite în sfârșit un răspuns către solicitant.
- Modele care descriu datele noastre și ajută la verificări la compilare.
Vom adăuga, de asemenea, o bază de date foarte rudimentară care nu este deloc potrivită pentru producție. (Singurul său scop este acela de a face acest tutorial mai ușor de urmat, deschizând calea pentru următorul nostru articol de aprofundare în conexiunea și integrarea bazei de date cu MongoDB și Mongoose.)
Practic: primii pași cu DAO, DTO și baza noastră de date temporară
Pentru această parte a tutorialului nostru, baza noastră de date nici măcar nu va folosi fișiere. Pur și simplu va păstra datele utilizatorului într-o matrice, ceea ce înseamnă că datele se evaporă ori de câte ori părăsim Node.js. Acesta va accepta numai cele mai elementare operațiuni de creare, citire, actualizare și ștergere (CRUD).
Vom folosi două concepte aici:
- Obiecte de acces la date (DAO)
- Obiecte de transfer de date (DTO)
Acea diferență de o literă între acronime este esențială: un DAO este responsabil pentru conectarea la o bază de date definită și pentru efectuarea operațiunilor CRUD; un DTO este un obiect care deține datele brute pe care DAO le va trimite și le va primi de la baza de date.
Cu alte cuvinte, DTO-urile sunt obiecte care se conformează tipurilor de model de date, iar DAO-urile sunt serviciile care le folosesc.
În timp ce DTO-urile pot deveni mai complicate - reprezentând entități imbricate de baze de date, de exemplu - în acest articol, o singură instanță DTO va corespunde unei acțiuni specifice pe un singur rând de bază de date.
De ce DTO-uri?
Utilizarea DTO pentru ca obiectele noastre TypeScript să se conformeze modelelor noastre de date ajută la menținerea coerenței arhitecturale, așa cum vom vedea în secțiunea privind serviciile de mai jos. Dar există o avertizare esențială: nici DTO-urile, nici TypeScript-urile în sine nu promit vreun fel de validare automată a introducerii utilizatorului, deoarece aceasta ar trebui să aibă loc în timpul execuției. Când codul nostru primește intrarea utilizatorului la un punct final din API-ul nostru, acea intrare poate:
- Aveți câmpuri suplimentare
- Lipsesc câmpuri obligatorii (adică cele fără sufixe cu
?) - Au câmpuri în care datele nu sunt de tipul pe care l-am specificat în modelul nostru folosind TypeScript
TypeScript (și JavaScript-ul în care este transpilat) nu vor verifica în mod magic acest lucru pentru noi, așa că este important să nu uităm aceste validări, mai ales când deschideți API-ul pentru public. Pachetele precum ajv pot ajuta cu acest lucru, dar funcționează în mod normal prin definirea modelelor într-un obiect de schemă specific bibliotecii, mai degrabă decât TypeScript nativ. (Mangusta, discutată în articolul următor, va juca un rol similar în acest proiect.)
S-ar putea să vă gândiți: „Este cu adevărat cel mai bine să folosiți atât DAO-uri, cât și DTO-uri, în loc de ceva mai simplu?” Dezvoltatorul de întreprinderi Gunther Popp oferă un răspuns; veți dori să evitați DTO-urile în majoritatea proiectelor mai mici din lumea reală Express.js/TypeScript, cu excepția cazului în care vă puteți aștepta în mod rezonabil la scară pe termen mediu.
Dar chiar dacă nu sunteți pe cale să le utilizați în producție, acest exemplu de proiect este o oportunitate utilă pe drumul spre stăpânirea arhitecturii API TypeScript. Este o modalitate excelentă de a exersa utilizarea tipurilor TypeScript în moduri suplimentare și de a lucra cu DTO pentru a vedea cum se compară cu o abordare mai simplă atunci când adăugați componente și modele.
Modelul nostru API REST de utilizator la nivel TypeScript
Mai întâi vom defini trei DTO-uri pentru utilizatorul nostru. Să creăm un folder numit dto în dosarul users și să creăm acolo un fișier numit create.user.dto.ts care conține următoarele:
export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }Spunem că de fiecare dată când creăm un utilizator, indiferent de baza de date, acesta ar trebui să aibă un ID, o parolă și un e-mail și, opțional, un nume și un prenume. Aceste cerințe se pot modifica în funcție de cerințele de afaceri ale unui anumit proiect.
Pentru cererile PUT , dorim să actualizăm întregul obiect, astfel încât câmpurile noastre opționale sunt acum necesare. În același folder, creați un fișier numit put.user.dto.ts cu acest cod:
export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } Pentru cererile PATCH , putem folosi caracteristica Partial din TypeScript, care creează un nou tip prin copierea unui alt tip și făcând toate câmpurile sale opționale. În acest fel, fișierul patch.user.dto.ts trebuie să conțină doar următorul cod:
import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} Acum, să creăm baza de date temporară în memorie. Să creăm un folder numit daos în folderul users și să adăugăm un fișier numit users.dao.ts .
În primul rând, dorim să importăm DTO-urile pe care le-am creat:
import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';Acum, pentru a gestiona ID-urile noastre de utilizator, să adăugăm biblioteca shortid (folosind terminalul):
npm i shortid npm i --save-dev @types/shortid Înapoi în users.dao.ts , vom importa shortid:
import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); Acum putem crea o clasă numită UsersDao , care va arăta astfel:
class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); Folosind modelul singleton, această clasă va furniza întotdeauna aceeași instanță - și, în mod critic, aceeași matrice de users - atunci când o importăm în alte fișiere. Asta pentru că Node.js memorează în cache acest fișier oriunde este importat și toate importurile au loc la pornire. Adică, oricărui fișier care se referă la users.dao.ts va primi o referință la același new UsersDao() care este exportat prima dată când Node.js procesează acest fișier.
Vom vedea că acest lucru funcționează atunci când vom folosi această clasă mai departe în acest articol și vom folosi acest model obișnuit TypeScript/Express.js pentru majoritatea claselor de-a lungul proiectului.
Notă: un dezavantaj des citat al singletonilor este că sunt greu de scris teste unitare pentru ele. În cazul multora dintre clasele noastre, acest dezavantaj nu se va aplica, deoarece nu există variabile membre ale clasei care ar trebui resetate. Dar pentru cei unde ar fi, îl lăsăm ca un exercițiu pentru ca cititorul să ia în considerare abordarea acestei probleme cu utilizarea injecției de dependență.
Acum vom adăuga operațiunile CRUD de bază în clasă ca funcții. Funcția de creare va arăta astfel:
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }Citirea va fi în două variante, „citește toate resursele” și „citește una după ID”. Sunt codificate astfel:
async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } De asemenea, actualizarea va însemna fie suprascrierea întregului obiect (ca PUT ) sau doar părți ale obiectului (ca PATCH ):
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`; } După cum am menționat mai devreme, în ciuda declarației noastre UserDto din aceste semnături de funcție, TypeScript nu oferă verificarea tipului de rulare. Aceasta înseamnă că:
-
putUserById()are o eroare. Acesta va permite consumatorilor API să stocheze valori pentru câmpurile care nu fac parte din modelul definit de DTO. -
patchUserById()depinde de o listă duplicată de nume de câmpuri care trebuie păstrate sincronizate cu modelul. Fără aceasta, ar trebui să folosească obiectul care este actualizat pentru această listă. Aceasta ar însemna că ar ignora în tăcere valorile pentru câmpurile care fac parte din modelul definit de DTO, dar care nu au fost salvate înainte pentru această instanță de obiect special.
Dar ambele scenarii vor fi tratate corect la nivelul bazei de date în articolul următor.
Ultima operație, de ștergere a unei resurse, va arăta astfel:
async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }Ca bonus, știind că o condiție prealabilă pentru a crea un utilizator este validarea dacă e-mailul utilizatorului nu este duplicat, să adăugăm acum o funcție „obține utilizator prin e-mail”:
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ă: Într-un scenariu real, probabil că vă veți conecta la o bază de date folosind o bibliotecă preexistentă, cum ar fi Mongoose sau Sequelize, care va abstra toate operațiunile de bază de care ați putea avea nevoie. Din această cauză, nu intrăm în detaliile funcțiilor implementate mai sus.
Stratul nostru de servicii REST API
Acum că avem un DAO de bază, în memorie, putem crea un serviciu care va apela funcțiile CRUD. Deoarece funcțiile CRUD sunt ceva pe care fiecare serviciu care se va conecta la o bază de date va trebui să aibă, vom crea o interfață CRUD care conține metodele pe care dorim să le implementăm de fiecare dată când dorim să implementăm un nou serviciu.
În zilele noastre, IDE-urile cu care lucrăm au caracteristici de generare de cod pentru a adăuga funcțiile pe care le implementăm, reducând cantitatea de cod repetitiv pe care trebuie să-l scriem.
Un exemplu rapid folosind IDE-ul WebStorm:
IDE-ul evidențiază numele clasei MyService și sugerează următoarele opțiuni:
Opțiunea „Implementați toți membrii” schelete instantaneu funcțiile necesare pentru a se conforma interfeței CRUD :
Acestea fiind spuse, să creăm mai întâi interfața TypeScript, numită CRUD . În folderul nostru common , să creăm un folder numit interfaces și să adăugăm crud.interface.ts cu următoarele:
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>; } După aceasta, să creăm un folder de services în folderul users și să adăugăm acolo fișierul users.service.ts , care conține:
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(); Primul nostru pas aici a fost să importam DAO-ul nostru în memorie, dependența noastră de interfață și tipul TypeScript al fiecăruia dintre DTO-urile noastre, este timpul să implementăm UsersService ca un serviciu unic, același model pe care l-am folosit cu DAO-ul nostru.
Toate funcțiile CRUD apelează acum funcțiile respective ale UsersDao . Când vine timpul să înlocuim DAO, nu va trebui să facem modificări în altă parte a proiectului, cu excepția unor ajustări ale acestui fișier în care sunt apelate funcțiile DAO, așa cum vom vedea în partea 3.
De exemplu, nu va trebui să urmărim fiecare apel la list() și să verificăm contextul înainte de a-l înlocui. Acesta este avantajul de a avea acest strat de separare, cu prețul cantității mici de boilerplate inițial pe care o vedeți mai sus.
Async/Await și Node.js
Utilizarea de către noi a async pentru funcțiile de serviciu poate părea inutilă. Pentru moment, este: Toate aceste funcții își returnează imediat valorile, fără nicio utilizare internă a Promise s sau await . Acest lucru este doar pentru a pregăti baza noastră de cod pentru serviciile care vor folosi async . La fel, mai jos, veți vedea că toate apelurile către aceste funcții folosesc await .
Până la sfârșitul acestui articol, veți avea din nou un proiect rulabil cu care să experimentați. Acesta va fi un moment excelent pentru a încerca să adăugați diferite tipuri de erori în diferite locuri din baza de cod și să vedeți ce se întâmplă în timpul compilării și testării. Erorile într-un context async în special pot să nu se comporte așa cum v-ați aștepta. Merită să explorați și să explorați diverse soluții, care depășesc scopul acestui articol.

Acum, având DAO și serviciile noastre pregătite, să revenim la controlerul utilizatorului.
Construirea controlerului nostru API REST
După cum am spus mai sus, ideea din spatele controlorilor este de a separa configurația rutei de codul care procesează în cele din urmă o cerere de rută. Asta înseamnă că toate validările ar trebui făcute înainte ca cererea noastră să ajungă la controlor. Controlorul trebuie să știe doar ce să facă cu cererea reală, deoarece dacă cererea a ajuns atât de departe, atunci știm că s-a dovedit a fi validă. Controlorul va apela apoi serviciul respectiv pentru fiecare solicitare pe care o va trata.
Înainte de a începe, va trebui să instalăm o bibliotecă pentru hashing în siguranță a parolei utilizatorului:
npm i argon2 Să începem prin a crea un folder numit controllers în interiorul folderului users controller și prin a crea un fișier numit users.controller.ts în el:
// 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ă: Rândurile de mai sus care nu trimit nimic înapoi cu un răspuns HTTP 204 No Content sunt în conformitate cu RFC 7231 pe acest subiect.
Cu un singur controler de utilizator finalizat, suntem gata să codificăm celălalt modul care depinde de modelul și serviciul nostru de obiect REST API: middleware-ul nostru pentru utilizator.
Node.js REST Middleware cu Express.js
Ce putem face cu middleware Express.js? Validarile se potrivesc foarte bine, unul. Să adăugăm câteva validări de bază pentru a acționa ca gardieni pentru solicitări înainte ca acestea să ajungă la controlerul nostru de utilizator:
- Asigurați-vă prezența câmpurilor de utilizator, cum ar fi e-
emailșipassword, după cum este necesar pentru a crea sau actualiza un utilizator - Asigurați-vă că un anumit e-mail nu este deja utilizat
- Verificați dacă nu schimbăm câmpul de
email-mail după creare (deoarece îl folosim ca ID principal pentru utilizator pentru simplitate) - Validați dacă există un anumit utilizator
Pentru ca aceste validări să funcționeze cu Express.js, va trebui să le transpunem în funcții care urmează modelul Express.js de control al fluxului folosind next() , așa cum este descris în articolul anterior. Vom avea nevoie de un fișier nou, 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();Cu familiarul singleton boilerplate din drum, să adăugăm câteva dintre funcțiile noastre middleware la corpul clasei:
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`, }); } } Pentru a face o modalitate ușoară pentru consumatorii noștri API de a face solicitări suplimentare despre un utilizator nou adăugat, vom adăuga o funcție de ajutor care va extrage userId -ul din parametrii solicitării - care vine de la adresa URL a cererii - și o va adăuga la corp de cerere, unde se află restul datelor utilizatorului.
Ideea aici este să putem folosi pur și simplu solicitarea întregului corp atunci când dorim să actualizăm informațiile utilizatorului, fără să ne facem griji cu privire la obținerea ID-ului din parametri de fiecare dată. În schimb, este îngrijit într-un singur loc, middleware-ul. Funcția va arăta astfel:
async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } Pe lângă logică, principala diferență dintre middleware și controler este că acum folosim funcția next() pentru a transmite controlul de-a lungul unui lanț de funcții configurate până când ajunge la destinația finală, care în cazul nostru este controlerul.
Punând totul împreună: refactorizarea rutelor noastre
Acum că am implementat toate aspectele noi ale arhitecturii proiectului nostru, să revenim la fișierul users.routes.config.ts pe care l-am definit în articolul anterior. Va apela middleware-ul nostru și controlerele noastre, ambele se bazează pe serviciul nostru pentru utilizatori, care, la rândul său, necesită modelul nostru de utilizator.
Fișierul final va fi la fel de simplu ca acesta:
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; } } Aici, ne-am redefinit rutele prin adăugarea de middleware pentru a valida logica noastră de afaceri și a funcțiilor de control corespunzătoare pentru a procesa cererea dacă totul este valid. De asemenea, am folosit funcția .param() din Express.js pentru a extrage userId .
La funcția .all() , transmitem funcția validateUserExists de la UsersMiddleware pentru a fi apelată înainte ca orice GET , PUT , PATCH sau DELETE să poată trece prin punctul final /users/:userId . Aceasta înseamnă că validateUserExists nu trebuie să fie în matricele de funcții suplimentare pe care le trecem la .put() sau .patch() — va fi apelată înainte de funcțiile specificate acolo.
Am profitat de reutilizarea inerentă a middleware-ului și în alt mod aici. Prin transmiterea UsersMiddleware.validateRequiredUserBodyFields pentru a fi utilizate atât în contexte POST , cât și în context PUT , îl recombinăm elegant cu alte funcții middleware.
Limitări de răspundere: acoperim doar validările de bază în acest articol. Într-un proiect din lumea reală, va trebui să vă gândiți și să găsiți toate restricțiile de care aveți nevoie pentru a codifica. De dragul simplității, presupunem, de asemenea, că un utilizator nu își poate schimba adresa de e-mail.
Testarea API-ului nostru Express/TypeScript REST
Acum putem compila și rula aplicația noastră Node.js. Odată ce rulează, suntem gata să testăm rutele noastre API folosind un client REST, cum ar fi Postman sau cURL.
Să încercăm mai întâi să atragem utilizatorii noștri:
curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'În acest moment, vom avea o matrice goală ca răspuns, care este precis. Acum putem încerca să creăm prima resursă utilizator cu aceasta:
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'Rețineți că acum aplicația noastră Node.js va trimite înapoi o eroare din middleware-ul nostru:
{ "error": "Missing required fields email and password" } Pentru a remedia problema, haideți să trimitem o solicitare validă de postare la resursa /users :
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'De data aceasta, ar trebui să vedem ceva de genul următor:
{ "id": "ksVnfnPVW" } Acest id este identificatorul noului utilizator creat și va fi diferit pe computer. Pentru a ușura instrucțiunile de testare rămase, puteți rula această comandă cu cea pe care o obțineți (presupunând că utilizați un mediu asemănător Linux):
REST_API_EXAMPLE_ Acum putem vedea răspunsul pe care îl primim din efectuarea unei cereri GET folosind variabila de mai sus:
curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' Acum putem actualiza întreaga resursă cu următoarea solicitare PUT :
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 }'De asemenea, putem testa dacă validarea noastră funcționează prin schimbarea adresei de e-mail, ceea ce ar trebui să aibă ca rezultat o eroare.
Rețineți că atunci când folosim un PUT la un ID de resursă, noi, ca consumatori API, trebuie să trimitem întregul obiect dacă dorim să ne conformăm modelului REST standard. Asta înseamnă că, dacă vrem să actualizăm doar câmpul lastName , dar folosind punctul nostru final PUT , vom fi forțați să trimitem întregul obiect pentru a fi actualizat. Ar fi mai ușor să utilizați o solicitare PATCH , deoarece există încă în constrângerile REST standard pentru a trimite doar câmpul lastName :
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' Amintiți-vă că în propria noastră bază de cod, configurația rutei noastre este cea care impune această distincție între PUT și PATCH folosind funcțiile middleware pe care le-am adăugat în acest articol.
PUT , PATCH sau Ambele?
Poate părea că nu există prea multe motive pentru a sprijini PUT , având în vedere flexibilitatea PATCH , iar unele API-uri vor adopta această abordare. Alții pot insista să susțină PUT pentru a face API-ul „complet conform REST”, caz în care, crearea de rute PUT pe câmp ar putea fi o tactică adecvată pentru cazurile de utilizare obișnuite.
În realitate, aceste puncte fac parte dintr-o discuție mult mai amplă, de la diferențele din viața reală dintre cele două până la o semantică mai flexibilă doar pentru PATCH . Vă prezentăm aici suportul PUT și semantica PATCH utilizată pe scară largă pentru simplitate, dar încurajăm cititorii să se aprofundeze în cercetări ulterioare atunci când se simt gata să facă acest lucru.
Obținând din nou lista de utilizatori așa cum am făcut mai sus, ar trebui să vedem utilizatorul creat cu câmpurile sale actualizate:
[ { "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 } ]În cele din urmă, putem testa ștergerea utilizatorului cu asta:
curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'Obținând din nou lista de utilizatori, ar trebui să vedem că utilizatorul șters nu mai este prezent.
Cu asta, avem toate operațiunile CRUD pentru utilizarea resurselor users .
API-ul REST Node.js/TypeScript
În această parte a seriei, am explorat în continuare pașii cheie în construirea unui API REST folosind Express.js. Ne împărțim codul pentru a suporta servicii, middleware, controlere și modele. Fiecare dintre funcțiile lor are un rol specific, fie că este vorba de validare, operațiuni logice sau de procesare a cererilor valide și de a le răspunde.
Am creat, de asemenea, o modalitate foarte simplă de stocare a datelor, cu scopul expres (scuzați jocul de cuvinte) de a permite unele teste în acest moment, apoi fiind înlocuit cu ceva mai practic în următoarea parte a seriei noastre.
Pe lângă construirea unui API având în vedere simplitate - folosind clase singleton, de exemplu -, există mai mulți pași de urmat pentru a-l face mai ușor de întreținut, mai scalabil și mai sigur. În ultimul articol din serie, acoperim:
- Înlocuirea bazei de date din memorie cu MongoDB, apoi folosirea Mongoose pentru a simplifica procesul de codare
- Adăugarea unui nivel de securitate și acces de control într-o abordare fără stat cu JWT
- Configurarea testării automate pentru a permite aplicației noastre să se extindă
Puteți răsfoi codul final din acest articol aici.
