Creación de una API REST de Node.js/TypeScript, parte 2: modelos, middleware y servicios
Publicado: 2022-03-11En el primer artículo de nuestra serie API REST, cubrimos cómo usar npm para crear un back-end desde cero, agregar dependencias como TypeScript, usar el módulo de debug integrado en Node.js, crear una estructura de proyecto Express.js y registrar el tiempo de ejecución. eventos de forma flexible con Winston. Si ya se siente cómodo con esos conceptos, simplemente clone esto, cambie a la toptal-article-01 con git checkout y siga leyendo.
Servicios API REST, Middleware, Controladores y Modelos
Como prometimos, ahora entraremos en detalles sobre estos módulos:
- Servicios que hacen que nuestro código sea más limpio al encapsular operaciones de lógica empresarial en funciones a las que pueden llamar el middleware y los controladores.
- Middleware que validará las condiciones previas antes de que Express.js llame a la función de controlador correspondiente.
- Controladores que usan servicios para procesar la solicitud antes de enviar finalmente una respuesta al solicitante.
- Modelos que describen nuestros datos y ayudan en las comprobaciones en tiempo de compilación.
También agregaremos una base de datos muy rudimentaria que de ninguna manera es adecuada para la producción. (Su único propósito es hacer que este tutorial sea más fácil de seguir, allanando el camino para que nuestro próximo artículo profundice en la conexión de la base de datos y la integración con MongoDB y Mongoose).
Práctico: primeros pasos con DAO, DTO y nuestra base de datos temporal
Para esta parte de nuestro tutorial, nuestra base de datos ni siquiera usará archivos. Simplemente mantendrá los datos del usuario en una matriz, lo que significa que los datos se evaporan cada vez que salimos de Node.js. Solo admitirá las operaciones más básicas de creación, lectura, actualización y eliminación (CRUD).
Aquí vamos a utilizar dos conceptos:
- Objetos de acceso a datos (DAO)
- Objetos de transferencia de datos (DTO)
Esa diferencia de una letra entre los acrónimos es esencial: un DAO es responsable de conectarse a una base de datos definida y realizar operaciones CRUD; un DTO es un objeto que contiene los datos sin procesar que el DAO enviará y recibirá de la base de datos.
En otras palabras, los DTO son objetos que se ajustan a los tipos de modelos de datos y los DAO son los servicios que los utilizan.
Si bien los DTO pueden volverse más complicados (representar entidades de bases de datos anidadas, por ejemplo), en este artículo, una sola instancia de DTO corresponderá a una acción específica en una sola fila de la base de datos.
¿Por qué DTO?
El uso de DTO para que nuestros objetos TypeScript se ajusten a nuestros modelos de datos ayuda a mantener la coherencia arquitectónica, como veremos en la sección sobre servicios a continuación. Pero hay una advertencia crucial: ni los DTO ni el propio TypeScript prometen ningún tipo de validación automática de la entrada del usuario, ya que eso tendría que ocurrir en tiempo de ejecución. Cuando nuestro código recibe la entrada del usuario en un punto final de nuestra API, esa entrada puede:
- Tener campos adicionales
- Faltan campos obligatorios (es decir, los que no tienen el sufijo
?) - Tener campos en los que los datos no son del tipo que especificamos en nuestro modelo usando TypeScript
TypeScript (y el JavaScript al que se transpila) no comprobará esto por arte de magia, por lo que es importante no olvidar estas validaciones, especialmente al abrir su API al público. Los paquetes como ajv pueden ayudar con esto, pero normalmente funcionan definiendo modelos en un objeto de esquema específico de la biblioteca en lugar de TypeScript nativo. (La mangosta, discutida en el próximo artículo, desempeñará un papel similar en este proyecto).
Podría estar pensando: "¿Es realmente mejor usar DAO y DTO, en lugar de algo más simple?" El desarrollador empresarial Gunther Popp ofrece una respuesta; querrá evitar los DTO en la mayoría de los proyectos Express.js/TypeScript del mundo real más pequeños, a menos que pueda esperar razonablemente escalar a mediano plazo.
Pero incluso si no está a punto de usarlos en producción, este proyecto de ejemplo es una oportunidad que vale la pena en el camino hacia el dominio de la arquitectura API de TypeScript. Es una excelente manera de practicar el aprovechamiento de los tipos de TypeScript de formas adicionales y trabajar con DTO para ver cómo se comparan con un enfoque más básico al agregar componentes y modelos.
Nuestro modelo de API REST de usuario en el nivel de TypeScript
Primero definiremos tres DTO para nuestro usuario. Vamos a crear una carpeta llamada dto dentro de la carpeta de users y crear un archivo allí llamado create.user.dto.ts que contenga lo siguiente:
export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; }Estamos diciendo que cada vez que creamos un usuario, independientemente de la base de datos, debe tener una identificación, contraseña y correo electrónico, y opcionalmente un nombre y apellido. Estos requisitos pueden cambiar en función de los requisitos comerciales de un proyecto determinado.
Para las solicitudes PUT , queremos actualizar todo el objeto, por lo que nuestros campos opcionales ahora son obligatorios. En la misma carpeta, cree un archivo llamado put.user.dto.ts con este código:
export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } Para las solicitudes PATCH , podemos usar la función Partial de TypeScript, que crea un nuevo tipo copiando otro tipo y haciendo que todos sus campos sean opcionales. De esa forma, el archivo patch.user.dto.ts solo necesita contener el siguiente código:
import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} Ahora, creemos la base de datos temporal en memoria. Creemos una carpeta llamada daos dentro de la carpeta de users y agreguemos un archivo llamado users.dao.ts .
Primero, queremos importar los DTO que creamos:
import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto';Ahora, para manejar nuestros ID de usuario, agreguemos la biblioteca shortid (usando la terminal):
npm i shortid npm i --save-dev @types/shortid De vuelta en users.dao.ts , importaremos shortid:
import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); Ahora podemos crear una clase llamada UsersDao , que se verá así:
class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); Usando el patrón singleton, esta clase siempre proporcionará la misma instancia y, lo que es más importante, la misma matriz de users , cuando la importemos en otros archivos. Esto se debe a que Node.js almacena en caché este archivo dondequiera que se importe, y todas las importaciones se realizan al inicio. Es decir, cualquier archivo que haga referencia a users.dao.ts recibirá una referencia al mismo new UsersDao() que se exporta la primera vez que Node.js procesa este archivo.
Veremos que esto funciona cuando usemos esta clase más adelante en este artículo y usemos este patrón común de TypeScript/Express.js para la mayoría de las clases a lo largo del proyecto.
Nota: Una desventaja frecuentemente citada de los singletons es que es difícil escribir pruebas unitarias para ellos. En el caso de muchas de nuestras clases, esta desventaja no se aplicará, ya que no hay variables de miembros de clase que deban restablecerse. Pero para aquellos en los que sí, lo dejamos como ejercicio para que el lector considere abordar este problema con el uso de la inyección de dependencia.
Ahora vamos a agregar las operaciones CRUD básicas a la clase como funciones. La función de creación se verá así:
async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; }La lectura vendrá en dos sabores, "leer todos los recursos" y "leer uno por ID". Están codificados así:
async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } Del mismo modo, la actualización significará sobrescribir el objeto completo (como PUT ) o solo partes del objeto (como 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`; } Como se mencionó anteriormente, a pesar de nuestra declaración UserDto en estas firmas de funciones, TypeScript no proporciona verificación de tipos en tiempo de ejecución. Esto significa que:
-
putUserById()tiene un error. Permitirá que los consumidores de API almacenen valores para campos que no forman parte del modelo definido por nuestro DTO. -
patchUserById()depende de una lista duplicada de nombres de campo que deben mantenerse sincronizados con el modelo. Sin esto, tendría que usar el objeto que se está actualizando para esta lista. Eso significaría que ignoraría silenciosamente los valores de los campos que forman parte del modelo definido por DTO pero que no se habían guardado antes para esta instancia de objeto en particular.
Pero ambos escenarios se manejarán correctamente a nivel de base de datos en el siguiente artículo.
La última operación, para eliminar un recurso, se verá así:
async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; }Como beneficio adicional, sabiendo que una condición previa para crear un usuario es validar si el correo electrónico del usuario no está duplicado, agreguemos una función de "obtener usuario por correo electrónico" ahora:
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; } }Nota: en un escenario del mundo real, probablemente se conectará a una base de datos utilizando una biblioteca preexistente, como Mongoose o Sequelize, que abstraerá todas las operaciones básicas que pueda necesitar. Debido a esto, no entraremos en los detalles de las funciones implementadas anteriormente.
Nuestra capa de servicios API REST
Ahora que tenemos un DAO básico en memoria, podemos crear un servicio que llamará a las funciones CRUD. Dado que las funciones CRUD son algo que todos los servicios que se conectarán a una base de datos deberán tener, vamos a crear una interfaz CRUD que contenga los métodos que queremos implementar cada vez que queramos implementar un nuevo servicio.
Hoy en día, los IDE con los que trabajamos tienen características de generación de código para agregar las funciones que estamos implementando, lo que reduce la cantidad de código repetitivo que necesitamos escribir.
Un ejemplo rápido usando el IDE de WebStorm:
El IDE resalta el nombre de la clase MyService y sugiere las siguientes opciones:
La opción "Implementar a todos los miembros" crea instantáneamente las funciones necesarias para ajustarse a la interfaz CRUD :
Dicho todo esto, primero creemos nuestra interfaz TypeScript, llamada CRUD . En nuestra carpeta common , creemos una carpeta llamada interfaces y agreguemos crud.interface.ts con lo siguiente:
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>; } Una vez hecho esto, creemos una carpeta de services dentro de la carpeta de users y agreguemos el archivo users.service.ts allí, que contiene:
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(); Nuestro primer paso aquí fue importar nuestro DAO en memoria, nuestra dependencia de interfaz y el tipo TypeScript de cada uno de nuestros DTO. Es hora de implementar UsersService como un servicio único, el mismo patrón que usamos con nuestro DAO.
Todas las funciones CRUD ahora solo llaman a las funciones respectivas de UsersDao . Cuando llegue el momento de reemplazar el DAO, no tendremos que hacer cambios en ningún otro lugar del proyecto, excepto algunos ajustes a este archivo donde se llaman las funciones DAO, como veremos en la Parte 3.
Por ejemplo, no tendremos que rastrear cada llamada a list() y verificar su contexto antes de reemplazarlo. Esa es la ventaja de tener esta capa de separación, a costa de la pequeña cantidad de repetitivo inicial que ve arriba.
Async/Await y Node.js
Nuestro uso de async para las funciones de servicio puede parecer inútil. Por ahora, es: todas estas funciones devuelven inmediatamente sus valores, sin ningún uso interno de Promise s o await . Esto es únicamente para preparar nuestra base de código para los servicios que usarán async . Del mismo modo, a continuación, verá que todas las llamadas a estas funciones usan await .
Al final de este artículo, volverá a tener un proyecto ejecutable para experimentar. Ese será un excelente momento para intentar agregar varios tipos de errores en diferentes lugares del código base y ver qué sucede durante la compilación y las pruebas. Es posible que los errores en un contexto async en particular no se comporten como cabría esperar. Vale la pena profundizar y explorar varias soluciones, que están más allá del alcance de este artículo.

Ahora, teniendo nuestro DAO y servicios listos, volvamos al controlador de usuario.
Construyendo nuestro controlador API REST
Como dijimos anteriormente, la idea detrás de los controladores es separar la configuración de la ruta del código que finalmente procesa una solicitud de ruta. Eso significa que todas las validaciones deben realizarse antes de que nuestra solicitud llegue al controlador. El controlador solo necesita saber qué hacer con la solicitud real porque si la solicitud llegó tan lejos, entonces sabemos que resultó ser válida. El controlador entonces llamará al servicio respectivo de cada solicitud que estará atendiendo.
Antes de comenzar, necesitaremos instalar una biblioteca para cifrar de forma segura la contraseña del usuario:
npm i argon2 Comencemos por crear una carpeta llamada controllers dentro de la carpeta de controladores de users y crear un archivo llamado users.controller.ts en ella:
// 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(); Nota: Las líneas anteriores que no devuelven nada con una respuesta HTTP 204 No Content están en línea con RFC 7231 sobre el tema.
Con nuestro singleton de controlador de usuario terminado, estamos listos para codificar el otro módulo que depende de nuestro modelo y servicio de objetos API REST de ejemplo: nuestro middleware de usuario.
Middleware REST de Node.js con Express.js
¿Qué podemos hacer con el middleware Express.js? Las validaciones encajan perfectamente, por ejemplo. Agreguemos algunas validaciones básicas para actuar como guardianes de las solicitudes antes de que lleguen a nuestro controlador de usuario:
- Asegurar la presencia de campos de usuario como
emailypasswordsegún sea necesario para crear o actualizar un usuario - Asegúrese de que un correo electrónico dado no esté en uso ya
- Verifique que no estemos cambiando el campo de
emaildespués de la creación (ya que lo estamos usando como la ID principal para el usuario por simplicidad) - Validar si existe un usuario dado
Para que estas validaciones funcionen con Express.js, necesitaremos convertirlas en funciones que sigan el patrón de control de flujo de Express.js usando next() , como se describe en el artículo anterior. Necesitaremos un nuevo archivo, 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();Con el familiar repetitivo singleton fuera del camino, agreguemos algunas de nuestras funciones de middleware al cuerpo de la clase:
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`, }); } } Para facilitar que nuestros consumidores de API realicen más solicitudes sobre un usuario recién agregado, vamos a agregar una función de ayuda que extraerá el userId de usuario de los parámetros de la solicitud, provenientes de la propia URL de la solicitud, y lo agregará a la cuerpo de la solicitud, donde residen el resto de los datos del usuario.
La idea aquí es poder simplemente usar la solicitud de cuerpo completo cuando nos gustaría actualizar la información del usuario, sin preocuparnos por obtener la ID de los parámetros cada vez. En cambio, se ocupa de ello en un solo lugar, el middleware. La función se verá así:
async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } Además de la lógica, la principal diferencia entre el middleware y el controlador es que ahora estamos usando la función next() para pasar el control a lo largo de una cadena de funciones configuradas hasta llegar al destino final, que en nuestro caso es el controlador.
Poniendo todo junto: refactorizando nuestras rutas
Ahora que hemos implementado todos los aspectos nuevos de la arquitectura de nuestro proyecto, volvamos al archivo users.routes.config.ts que definimos en el artículo anterior. Llamará a nuestro middleware y a nuestros controladores, los cuales dependen de nuestro servicio de usuario, que a su vez requiere nuestro modelo de usuario.
El archivo final será tan simple como esto:
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; } } Aquí, hemos redefinido nuestras rutas agregando middleware para validar nuestra lógica comercial y las funciones de controlador apropiadas para procesar la solicitud si todo es válido. También usamos la función .param() de Express.js para extraer el userId de usuario.
En la función .all() , estamos pasando nuestra función validateUserExists de UsersMiddleware para que se llame antes de que cualquier GET , PUT , PATCH o DELETE pueda pasar en el punto final /users/:userId . Esto significa que validateUserExists no necesita estar en las matrices de funciones adicionales que pasamos a .put() o .patch() ; se llamará antes que las funciones especificadas allí.
También hemos aprovechado la reutilización inherente del middleware aquí de otra manera. Al pasar UsersMiddleware.validateRequiredUserBodyFields para usar en contextos POST y PUT , lo estamos recombinando elegantemente con otras funciones de middleware.
Descargos de responsabilidad: solo cubrimos validaciones básicas en este artículo. En un proyecto del mundo real, deberá pensar y encontrar todas las restricciones que necesita para codificar. En aras de la simplicidad, también asumimos que un usuario no puede cambiar su correo electrónico.
Prueba de nuestra API REST de Express/TypeScript
Ahora podemos compilar y ejecutar nuestra aplicación Node.js. Una vez que se está ejecutando, estamos listos para probar nuestras rutas API utilizando un cliente REST como Postman o cURL.
Primero intentemos conseguir a nuestros usuarios:
curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json'En este punto, tendremos una matriz vacía como respuesta, lo cual es exacto. Ahora podemos intentar crear el primer recurso de usuario con esto:
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json'Tenga en cuenta que ahora nuestra aplicación Node.js devolverá un error de nuestro middleware:
{ "error": "Missing required fields email and password" } Para solucionarlo, enviemos una solicitud válida para publicar en el recurso /users :
curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "[email protected]", "password": "sup3rS3cr3tPassw0rd!23" }'Esta vez, deberíamos ver algo como lo siguiente:
{ "id": "ksVnfnPVW" } Esta id es el identificador del usuario recién creado y será diferente en su máquina. Para facilitar las declaraciones de prueba restantes, puede ejecutar este comando con el que obtiene (suponiendo que está utilizando un entorno similar a Linux):
REST_API_EXAMPLE_ Ahora podemos ver la respuesta que obtenemos al realizar una solicitud GET utilizando la variable anterior:
curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' Ahora también podemos actualizar todo el recurso con la siguiente solicitud 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 }'También podemos probar que nuestra validación funciona cambiando la dirección de correo electrónico, lo que debería dar como resultado un error.
Tenga en cuenta que cuando usamos PUT para un ID de recurso, nosotros, como consumidores de API, debemos enviar el objeto completo si queremos cumplir con el patrón REST estándar. Eso significa que si queremos actualizar solo el campo lastName , pero usando nuestro punto final PUT , nos veremos obligados a enviar todo el objeto para que se actualice. Sería más fácil usar una solicitud PATCH ya que todavía está dentro de las restricciones REST estándar para enviar solo el campo lastName :
curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' Recuerde que en nuestro propio código base, es nuestra configuración de ruta la que impone esta distinción entre PUT y PATCH usando las funciones de middleware que agregamos en este artículo.
PUT , PATCH , o ambos?
Puede parecer que no hay muchas razones para admitir PUT dada la flexibilidad de PATCH , y algunas API adoptarán ese enfoque. Otros pueden insistir en admitir PUT para hacer que la API sea "completamente compatible con REST", en cuyo caso, la creación de rutas PUT por campo podría ser una táctica adecuada para casos de uso común.
En realidad, estos puntos son parte de una discusión mucho más amplia que va desde las diferencias de la vida real entre los dos hasta una semántica más flexible solo para PATCH . Presentamos el soporte PUT aquí y la semántica PATCH ampliamente utilizada para simplificar, pero alentamos a los lectores a profundizar en la investigación cuando se sientan listos para hacerlo.
Obteniendo la lista de usuarios nuevamente como lo hicimos anteriormente, deberíamos ver nuestro usuario creado con sus campos actualizados:
[ { "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 } ]Finalmente, podemos probar eliminando al usuario con esto:
curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json'Obteniendo la lista de usuarios nuevamente, deberíamos ver que el usuario eliminado ya no está presente.
Con eso, tenemos todas las operaciones CRUD para el recurso de los users funcionando.
API REST de Node.js/TypeScript
En esta parte de la serie, exploramos más a fondo los pasos clave para crear una API REST con Express.js. Dividimos nuestro código para admitir servicios, middleware, controladores y modelos. Cada una de sus funciones tiene un rol específico, ya sea validación, operaciones lógicas o procesamiento de solicitudes válidas y respuesta a ellas.
También creamos una forma muy simple de almacenar datos, con el propósito expreso (perdón por el juego de palabras) de permitir algunas pruebas en este punto, y luego ser reemplazada por algo más práctico en la siguiente parte de nuestra serie.
Además de crear una API pensando en la simplicidad (usando clases singleton, por ejemplo), hay varios pasos a seguir para que sea más fácil de mantener, más escalable y segura. En el último artículo de la serie, cubrimos:
- Reemplazar la base de datos en memoria con MongoDB, luego usar Mongoose para simplificar el proceso de codificación
- Agregar una capa de seguridad y controlar el acceso en un enfoque sin estado con JWT
- Configuración de pruebas automatizadas para permitir que nuestra aplicación escale
Puede consultar el código final de este artículo aquí.
