Cómo construir una REST API CRUD en memoria con Nest y desplegarla en Vercel

En mi artículo anterior dimos los primeros pasos creando APIs en Nest. Aprendimos a crear un controlador y a sumarle métodos con decoradores para recibir y enviar datos, lo cual fue un ejemplo bastante básico. Cuando queremos construir mejores ejemplos o un MVP real, permitir operaciones de creación, lectura, actualización y eliminación (CRUD) resulta mucho más útil que solo leer datos.
Hoy daremos un paso adelante: implementaremos operaciones CRUD completas en nuestra API de ejemplo y la desplegaremos en Vercel.
El escenario
En el artículo anterior creamos el proyecto jumbo_api. Tenía un controlador de tarjetas (CardsController) con tres métodos:
list: retorna una lista fija (hardcoded) de tarjetas.generate/:name: recibe el parámetronamey retorna una entidad sin tipar con ese nombre, manejando la lógica dentro del mismo método.subscribe: recibe el cuerpo de la petición (body) mediante POST y retorna un objeto sin tipar con esos datos y propiedades adicionales.
Si queremos implementar operaciones CRUD y mejorar la calidad del código, podemos seguir estos pasos:
- Asegurar que siempre se retorne una entidad de tarjeta tipada.
- Extraer la lógica de negocio fuera del controlador.
- Almacenar los datos en memoria y retornarlos.
- Agregar endpoints para actualizar y eliminar datos.
¿Parece demasiado trabajo? Para nada. Nest nos facilita bastante el camino.

Configuración del proyecto
Clona el proyecto anterior, ve a la carpeta jumbo_api (que contiene el controlador de tarjetas), ejecuta el comando npm i y ábrelo en tu editor de código favorito.
Para seguir este artículo fácilmente, utiliza la rama
start_point.
git clone https://github.com/danywalls/jumbo_project.git
git checkout start_point
cd jumbo
npm iPerfecto, ya tenemos todo listo. Ahora pasemos a extraer la lógica del controlador.
Entidad Card
En el artículo inicial teníamos los métodos getAll, generateCard y subscribe, y cada uno devolvía un objeto sin tipado formal, a veces con propiedades faltantes. Este enfoque no es bueno para un CRUD, ya que queremos asegurarnos de contar con todas las propiedades necesarias.
Podemos solucionar esto creando un tipo de entidad que represente nuestra tarjeta. Esto funcionará como un contrato claro para nuestros métodos y para quien consuma la API.
Ve al directorio cards y crea el archivo cards.entity.ts:
export type Card = {
id: number;
code: string;
validDate: string;
holder: string;
};Excelente, ahora ya tenemos un contrato para nuestros métodos y respuestas. El siguiente paso es mover la lógica fuera del controlador.
El servicio CRUD
En Nest, al igual que en Angular, podemos apoyarnos en un servicio para separar la lógica de los controladores. Nest utiliza el controlador para recibir y gestionar las peticiones HTTP, pero cualquier procesamiento o regla de negocio debe manejarse en un servicio.
El servicio se encarga de la lógica de la aplicación, lo que nos permite reutilizarla en otros lugares de la API si fuera necesario. Para crear un servicio, usamos el CLI de Nest con el comando nest g s:
dany ~/Desktop/jumbo/jumbo_api [main] $ nest g s
? What name would you like to use for the service? cards
CREATE src/cards/cards.service.spec.ts (453 bytes)
CREATE src/cards/cards.service.ts (89 bytes)
UPDATE src/app.module.ts (394 bytes)El CLI lo registra automáticamente en la sección de providers dentro de app.module.ts. Si vienes de Angular esto te resultará familiar, pero si no es tu caso, déjame explicarte brevemente qué hace.
La sección de providers nos permite registrar clases decoradas con @Injectable(). Cuando otra clase solicita uno de estos tipos en su constructor, el sistema de inyección de dependencias de Nest se encarga de proveer una instancia.
¿Cómo inyectamos cards.service en cards.controller.ts? En el constructor declaramos un nuevo parámetro, por ejemplo cardService de tipo CardsService. Nest identificará que CardsController necesita esa instancia y se la entregará automáticamente:
....
import { CardsService } from './cards.service';
@Controller('cards')
export class CardsController {
constructor(private readonly cardService: CardsService) {}Agregar las acciones CRUD al servicio
Ya tenemos el servicio creado, pero ahora debe ofrecer las acciones CRUD y mantener un estado en memoria para el controlador. Primero creamos un arreglo de tarjetas que funcionará como nuestro almacenamiento temporal:
private cards: Array<Card> = [];Usando este arreglo como almacenamiento, debemos implementar los siguientes métodos:
getAll: retorna todas las tarjetas guardadas en el arreglo.getById: retorna la tarjeta que coincida con el ID recibido.create: crea un objeto de tarjeta y lo guarda en el arreglo.update: actualiza una tarjeta individual; busca si existe y aplica los cambios.delete: recibe el ID por parámetro, busca su posición en el arreglo y la elimina si existe.
Vamos a escribir el código de cada uno.
GetAll
Este método no recibe parámetros y retorna el arreglo privado cards:
getAll(): Array<Card> {
return this.cards;
}GetById
Recibe el ID de la tarjeta como parámetro y usa el método find del arreglo para devolver la tarjeta correspondiente:
getById(id: string): Card {
return this.cards.find((card) => card.id === +id);
}Create
Este método recibe una tarjeta de tipo Card y la guarda en el arreglo:
create(card: Card) {
card.id = Math.random();
card.validDate = new Date().toLocaleDateString('ES');
this.cards = [...this.cards, card];
}Espera un segundo... si el método create espera una tarjeta completa de tipo Card, ¿significa que el controlador tendría que encargarse de generar el ID y la fecha de validez?
El propósito del servicio es quitarle esa lógica al controlador. Por ahora cambiaremos el tipo a any (o campos opcionales) mientras estructuramos el flujo:
Mejoraremos esto más adelante, ya que recurrir a
anyno es una buena práctica a largo plazo.
create(card: any) {
card.id = Math.random();
card.validDate = new Date().toLocaleDateString('ES');
this.cards = [...this.cards, card];
}Update
Este método recibe dos parámetros: el ID de la tarjeta a modificar y los nuevos valores. Al igual que en create, no queremos obligar al controlador a enviar todas las propiedades, así que temporalmente usamos any (algo que corregiremos luego):
update(id: string, updateCard: any) {
const indexToUpdate = this.cards.findIndex((card) => card.id === +id);
if (indexToUpdate >= 0) {
this.cards[indexToUpdate] = {
...this.cards[indexToUpdate],
...updateCard,
};
}
}En el código buscamos el índice de la tarjeta. Si existe, desestructuramos el valor actual y lo combinamos con las propiedades recibidas en updateCard.
Delete
Este método recibe el ID de la tarjeta, usa findIndex para ubicar la posición en el arreglo y aplica splice para removerla:
delete(id: string) {
const cardIndex = this.cards.findIndex((c) => c.id === +id);
if (cardIndex >= 0) {
this.cards.splice(cardIndex, 1);
}
}Perfecto. Con todos los métodos listos en el servicio, pasemos a conectarlo con nuestro controlador.
Integrar el servicio con el controlador
Como ya inyectamos CardsService en el constructor, vamos a actualizar los endpoints existentes y agregar los nuevos para aprovechar la funcionalidad del servicio:
- Actualizar los endpoints
@Geten/listy/list/:id, leyendo los parámetros con el decorador@Param(). - Actualizar el endpoint
@Posten el métodosubscribe, obteniendo los valores con@Body().
GetAll y GetById
Modificamos el método getAll en el controlador para invocar this.cardService.getAll() y obtener el listado de tarjetas.
Luego creamos el nuevo endpoint list/:id, que recibe el id como parámetro y lo envía al método getById del servicio:
@Get('list')
getAll(): Array<Card> {
return this.cardService.getAll();
}
@Get('list/:id')
getById(@Param('id') id: string) {
this.cardService.getById(id);
}Create
El método subscribe utiliza el decorador @Body() para extraer los datos del payload de la petición POST y pasarlos directamente a this.cardService.create(body):
@Post('subscribe')
@HttpCode(HttpStatus.OK)
subscribe(@Body() body) {
this.cardService.create(body);
}Update y Delete
NestJS ofrece decoradores dedicados para las acciones de actualización y eliminación. En el caso de la actualización, hay una distinción clave que conviene tener clara.
En las APIs REST existen dos verbos comunes para actualizar datos: PUT y PATCH:
PUT: reemplaza el recurso por completo, por lo que se debe enviar el objeto entero en el cuerpo de la petición.PATCH: modifica el recurso de forma parcial, permitiendo actualizar solo los campos necesarios.
Nest nos ofrece decoradores directos para trabajar con estos verbos: @Patch() y @Put().
Si trabajas con formularios reactivos en Angular, esto te recordará la diferencia entre
patchValueysetValue.
Creamos el método update con el decorador @Patch('subscribe/:id'). Recibimos el id con @Param('id') y los datos con @Body(), y se los pasamos a this.cardService.update:
@Patch('subscribe/:id')
update(@Param('id') id: string, @Body() body) {
this.cardService.update(id, body);
}Por último, para la eliminación usamos el decorador @Delete(':id'), capturamos el ID y lo pasamos al método delete del servicio:
@Delete(':id')
delete(@Param('id') id: string) {
this.cardService.delete(id);
}Listo, terminamos de integrar el servicio en nuestro controlador. Guarda los cambios y ejecuta npm run start para probar la API con herramientas como Postman o Bruno. Con el servidor corriendo, veamos la API en acción realizando peticiones reales.
La API en acción
De manera similar a lo que hicimos en el artículo anterior, interactuamos con los distintos verbos HTTP:
POST: enviamos una petición POST con la propiedad holder en el cuerpo (body):

Para actualizar, primero necesitamos conocer el ID de la tarjeta, así que consultamos el endpoint cards/list:

Tomamos el ID y enviamos una petición PATCH al endpoint cards/subscribe/tu_id, enviando el nuevo nombre de holder en el body:

Para confirmar el cambio, volvemos a consultar cards/list con una petición GET y veremos que el recurso retorna con la información actualizada:

Finalmente, tomamos nuevamente el ID y enviamos una petición DELETE al endpoint cards/tu_id:

Para comprobarlo, llamamos nuevamente al endpoint cards/list con GET y veremos un arreglo vacío:

¡Genial! Hemos ampliado la API para crear, modificar y eliminar datos en memoria (al menos mientras no reinicies el servidor local). Ahora pasemos a publicarla en internet.
Despliegue en Vercel
Un punto que quedó pendiente en el artículo anterior y que varios lectores me pidieron fue dónde y cómo desplegar la API. En mi caso, recomiendo desplegarla en Vercel porque el proceso es muy directo y rápido.
Primero, necesitas una cuenta en Vercel conectada con tu cuenta de GitHub. En la raíz de jumbo_api, creamos un archivo llamado vercel.json:
{
"version": 2,
"builds": [
{
"src": "src/main.ts",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "src/main.ts",
"methods": ["GET", "POST", "PUT", "DELETE"]
}
]
}Guarda los cambios y haz push a tu repositorio. Luego entra a https://vercel.com/new/import para importar tu proyecto:

Como tenemos dos proyectos dentro del repositorio jumbo, debemos asegurarnos de seleccionar la carpeta jumbo_api:

Selecciona el proyecto, haz clic en el botón Continue y luego en Deploy. Espera a que termine el proceso de compilación:

Una vez completado el despliegue, aparecerá una pantalla con confeti confirmando que tu API está en producción.
Puedes probar la versión en vivo que dejé funcionando aquí: https://jumbo-project-bzku.vercel.app/cards/list
Conclusión
En este artículo dimos un paso adelante en la creación de APIs con Nest al implementar operaciones CRUD en jumbo_api. Creamos la entidad Card, extrajimos la lógica del controlador hacia un servicio, almacenamos datos en memoria y agregamos endpoints para actualizar y eliminar recursos. Además, vimos lo sencillo que resulta desplegar la aplicación en Vercel.
Si quieres aprender más sobre estos temas, te invito a revisar la documentación oficial de NestJS o a suscribirte al blog para enterarte de los próximos artículos.
Si tienes curiosidad por revisar la implementación completa, puedes explorar el código fuente en GitHub.
Foto de Worachat Sodsri en Unsplash
Explora más guías prácticas y arquitecturas reales en la serie Cloud.
Preguntas Frecuentes
¿Cómo implemento operaciones CRUD en NestJS usando almacenamiento en memoria?
En NestJS puedes implementar un CRUD creando un servicio que maneje un arreglo o un Map como almacén de datos en memoria, exponiendo métodos para crear, leer, actualizar y eliminar. El controlador usa decoradores como @Get, @Post, @Patch o @Put y @Delete para conectar los métodos HTTP con el servicio, manteniendo la lógica de negocio separada de las rutas.
¿Cómo despliego una aplicación de NestJS en Vercel?
Para desplegar una app de NestJS en Vercel, agrega un archivo de configuración vercel.json que redirija las peticiones al punto de entrada de la aplicación. Vercel ejecuta Node.js mediante funciones serverless. Tras configurar el archivo, sube tu código a GitHub y conecta el repositorio en Vercel para que compile y despliegue automáticamente.
¿Cuál es el rol de un servicio en NestJS y en qué se diferencia de un controlador?
En NestJS, el controlador se encarga de recibir las peticiones HTTP y devolver las respuestas, mientras que el servicio contiene la lógica de negocio y el acceso a los datos. Separar estas responsabilidades hace que el código sea más modular, fácil de probar y reutilizable. Luego puedes inyectar el servicio en el controlador mediante el sistema de inyección de dependencias de Nest.
¿Cómo creo una entidad tipada en NestJS para un recurso CRUD?
Define un tipo, interfaz o clase de TypeScript que describa la estructura de tu recurso, como un tipo Card con propiedades como id, code, validDate y holder. Usa este tipo en los métodos de tu servicio para la creación, actualización y retornos. Esto le da al compilador de TypeScript control total de tipos, reduciendo errores en tiempo de ejecución.
Artículos Relacionados
Cómo adquirir habilidades Cloud siendo desarrollador Frontend
Soy desarrollador Frontend enfocado en Angular, TypeScript y Sass. Durante los últimos cuatro años enfoqué mi energía en mejorar mis habilidades frontend, pero sin aprender sobre Cloud. Te cuento por qué decidí cambiar esto.
De Vercel Next.js a Google Cloud Run: Guía de costos y arquitectura
Por qué migré mis proyectos de Next.js de Vercel a Google Cloud Run, cómo funciona la compilación standalone con Docker y cómo mantenerse en el nivel gratuito.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.