Cómo crear API REST con Node.js, TypeScript y Express

El otro día me dio por leer de nuevo mi artículo sobre Qué es una API y cómo trabajar con ella desde cero, pero sentí que quizás a la hora de crear API REST faltara información práctica. En aquel texto expliqué qué es una API y para qué sirve, pero si queremos llevar la teoría a la práctica necesitamos un camino mucho más detallado. Por eso hoy quiero mostrarte no solo las bases, sino todo el proceso técnico paso a paso: desde cómo configurar el entorno de desarrollo, hasta desplegar una API en un servidor con Docker, pasando por puntos clave como la autenticación de usuarios con JWT, la validación de datos, la creación de pruebas automáticas y el consumo de la API desde un frontend hecho en Angular.

En definitiva, la idea de este artículo es que cuando termines de leerlo tengas una guía completa para desarrollar una API REST profesional que puedas usar en tus proyectos personales o incluso en entornos de producción reales.


Qué significa crear API REST y cómo funciona

Cuando hablamos de crear API REST, nos referimos a diseñar un servicio que permita intercambiar información entre cliente y servidor usando el protocolo HTTP. La clave está en que este intercambio siga una serie de principios que lo hagan predecible, escalable y fácil de mantener.

Imagina, por ejemplo, que tienes una aplicación móvil de gestión de tareas. Cada vez que marcas una tarea como completada, la app no guarda ese cambio dentro del móvil, sino que envía una petición HTTP a un servidor. Ese servidor procesa la petición, guarda el cambio en una base de datos y luego responde con la confirmación de que la tarea se actualizó correctamente. Ese ida y vuelta de datos es lo que hace que la experiencia sea consistente sin importar desde qué dispositivo accedas.

Características clave de una API REST

Stateless
Cada petición se procesa de forma independiente, sin depender de lo que haya ocurrido antes. Por ejemplo, un GET /todos no necesita saber si antes hiciste un POST /todos. Esto hace que la API sea predecible y escalable, ya que no requiere mantener sesiones en memoria.

Uniformidad
Las rutas y respuestas deben seguir un patrón claro y coherente. Por ejemplo:

  • /users para usuarios.
  • /todos para tareas.
    Todas las respuestas deberían estar en JSON y mantener la misma estructura, facilitando su consumo por cualquier cliente.

Escalabilidad
Al ser stateless, una API REST puede ejecutarse en múltiples servidores y distribuir la carga sin problemas. Esto permite que aplicaciones grandes manejen miles de peticiones al mismo tiempo.

Separación cliente-servidor
El backend y el frontend son independientes. Un único servidor puede alimentar aplicaciones en Angular, React o incluso móviles en Flutter, sin modificar el núcleo de la API.

Métodos HTTP estándar
REST utiliza métodos bien definidos:

  • GET para obtener datos.
  • POST para crear.
  • PUT o PATCH para actualizar.
  • DELETE para eliminar.
    Esto hace que el comportamiento sea claro y predecible para los desarrolladores.

Códigos de estado HTTP
Además del contenido, la API debe devolver el código de estado correcto:

  • 200 OK → petición exitosa.
  • 201 Created → recurso creado.
  • 400 Bad Request → datos inválidos.
  • 401 Unauthorized → sin permisos.
  • 404 Not Found → recurso inexistente.

Ejemplo sencillo de rutas REST:

GET    /todos       → Lista todas las tareas
POST   /todos       → Crea una nueva tarea
GET    /todos/:id   → Devuelve una tarea por ID
PATCH  /todos/:id   → Actualiza una tarea
DELETE /todos/:id   → Elimina una tarea

Configuración inicial para crear API REST en Node.js

El primer paso para crear API REST de manera profesional es preparar bien el proyecto. En este caso, usamos Node.js con TypeScript y Express, lo que nos permite trabajar con tipado fuerte y un servidor rápido y ligero. La instalación de dependencias básicas (express, typescript, ts-node-dev, entre otras) es fundamental para empezar con una base sólida y evitar problemas más adelante.

mkdir api-rest-node
cd api-rest-node
npm init -y
npm install express cors jsonwebtoken bcryptjs zod
npm install prisma @prisma/client
npm install typescript ts-node-dev @types/node @types/express --save-dev
npx tsc --init

Estructura inicial:

src/
  index.ts
  features/
    todos/
      todos.routes.ts
  middlewares/
    errorHandler.ts
    authMiddleware.ts
  docs/
prisma/
  schema.prisma

👉 Esta organización modular permite escalar la API sin volverse un caos.


Conexión a la base de datos con Prisma

Para desarrollar una API REST moderna, Prisma resulta una herramienta ideal. Este ORM simplifica la comunicación con la base de datos, ya sea MySQL o PostgreSQL, evitando que tengamos que escribir consultas SQL complejas a mano. Definimos nuestros modelos en el archivo prisma/schema.prisma, y a partir de ahí Prisma genera código seguro y tipado que podemos usar directamente en el proyecto. Esto no solo mejora la productividad, sino que también reduce errores humanos.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id       Int    @id @default(autoincrement())
  email    String @unique
  password String
  todos    Todo[]
}

model Todo {
  id     Int     @id @default(autoincrement())
  title  String
  done   Boolean @default(false)
  user   User?   @relation(fields: [userId], references: [id])
  userId Int?
}

Migramos con:

npx prisma migrate dev --name init

Crear API REST con rutas CRUD completas

Un ejemplo básico de rutas puede ser la gestión de tareas. En Express, definimos un endpoint /todos para listar tareas, otro para crearlas y así sucesivamente. Organizar las rutas en ficheros separados y conectar con controladores hace que el proyecto sea más fácil de mantener y escalar.

import { Router } from "express";
import { PrismaClient } from "@prisma/client";

const router = Router();
const prisma = new PrismaClient();

// Obtener todos
router.get("/", async (_, res) => {
  const todos = await prisma.todo.findMany();
  res.json(todos);
});

// Crear tarea
router.post("/", async (req, res, next) => {
  try {
    const { title } = req.body;
    const todo = await prisma.todo.create({ data: { title } });
    res.status(201).json(todo);
  } catch (err) {
    next(err);
  }
});

// Actualizar
router.patch("/:id", async (req, res) => {
  const { id } = req.params;
  const { done } = req.body;
  const todo = await prisma.todo.update({
    where: { id: Number(id) },
    data: { done },
  });
  res.json(todo);
});

// Eliminar
router.delete("/:id", async (req, res) => {
  const { id } = req.params;
  await prisma.todo.delete({ where: { id: Number(id) } });
  res.status(204).send();
});

export default router;

👉 Con esto tienes tu primer CRUD REST completo.


Middleware de errores en API REST

Siempre que trabajamos con datos pueden surgir problemas: validaciones que fallan, un ID que no existe en la base de datos o un token caducado. Por eso, centralizar el manejo de errores es una buena práctica. De esta forma, la API devuelve siempre una respuesta clara y consistente, con el código de estado adecuado (400, 404, 500…), evitando duplicar lógica en cada ruta.

import { Request, Response, NextFunction } from "express";

export function errorHandler(err: any, _: Request, res: Response, __: NextFunction) {
  console.error(err);
  res.status(500).json({ error: "Algo salió mal en el servidor 😢" });
}

En index.ts:

import { errorHandler } from "./middlewares/errorHandler";
app.use(errorHandler);

Autenticación y seguridad con JWT

Para crear API REST segura, se suele usar JWT (JSON Web Tokens). Con ellos validamos la identidad del usuario en cada petición. Lo más importante es recordar que nunca debemos confiar en los datos enviados por el cliente: siempre hay que verificarlos, sanitizarlos y validar su formato. Así evitamos inyecciones, accesos no autorizados y otros problemas de seguridad comunes.

import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";

const JWT_SECRET = "mi_clave_secreta";

export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const auth = req.headers.authorization;
  if (!auth) return res.status(401).json({ error: "Token requerido" });

  const token = auth.split(" ")[1];
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    (req as any).user = decoded;
    next();
  } catch {
    return res.status(401).json({ error: "Token inválido" });
  }
}

👉 Así protegemos rutas sensibles:

router.get("/me", authMiddleware, async (req, res) => {
  res.json({ message: "Ruta protegida" });
});

Validación de datos con Zod

Nunca se debe confiar en los datos del cliente porque pueden ser alterados o enviados de forma maliciosa. Aunque un formulario frontend parezca seguro, siempre existe la posibilidad de que alguien manipule la petición con herramientas como Postman o directamente desde la consola del navegador. Por eso, es imprescindible validar y sanitizar la información en el backend antes de procesarla, asegurando que cumple el formato esperado y evitando riesgos como inyecciones o accesos no autorizados.

import { z } from "zod";

const todoSchema = z.object({
  title: z.string().min(3),
  done: z.boolean().optional(),
});

router.post("/", async (req, res) => {
  const parsed = todoSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(422).json(parsed.error.errors);
  }
  const todo = await prisma.todo.create({ data: parsed.data });
  res.status(201).json(todo);
});

👉 Así evitamos guardar datos corruptos en la base de datos.


Paginación y filtros en la API REST

Cuando la base de datos crece, no podemos devolver miles de registros en una sola petición. Aquí entra la paginación, que permite enviar la información poco a poco, mejorando la velocidad y la experiencia del cliente. Además, es clave para optimizar el rendimiento y reducir el consumo de recursos del servidor.

router.get("/", async (req, res) => {
  const page = parseInt(req.query.page as string) || 1;
  const limit = parseInt(req.query.limit as string) || 10;
  const skip = (page - 1) * limit;

  const todos = await prisma.todo.findMany({ skip, take: limit });
  res.json({ page, limit, data: todos });
});

👉 Esto evita que un GET devuelva miles de registros en una sola respuesta.


Documentar API REST con Swagger

Con Swagger dispones de una documentación interactiva accesible en la ruta /api-docs. Esta herramienta genera automáticamente una interfaz visual donde puedes ver y probar todos los endpoints de tu API sin necesidad de configuraciones externas.

Swagger OpenAPI permite:

  • Probar endpoints sin Postman, ya que desde la misma interfaz puedes enviar peticiones y comprobar las respuestas en tiempo real.
  • Ver inputs y outputs esperados, mostrando qué parámetros son necesarios, sus tipos de datos y cómo luce la respuesta que devuelve el servidor.
  • Compartir la API con otros equipos, lo que facilita la colaboración entre desarrolladores frontend, backend e incluso testers, al contar todos con una referencia clara y siempre actualizada.

Además, al integrarlo en el proyecto, cualquier cambio en las rutas se refleja de inmediato en la documentación, garantizando que nunca quede desactualizada.


Dockerizar la API REST

Una vez lista la API, llega el momento de llevarla a producción. Para ello usamos Docker, que nos permite ejecutar el proyecto en un contenedor aislado, asegurando que se comporte igual en cualquier servidor. Esto facilita el despliegue y la portabilidad, evitando el clásico “en mi máquina funciona, pero en el servidor no”.

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

Y docker-compose.yml:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/api
  db:
    image: postgres:15
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: api

👉 Con docker-compose up levantas API + base de datos en segundos.


MySQL vs PostgreSQL al desarrollar API REST

¿Cuál elegir?

  • MySQL → Es una base de datos muy extendida y conocida por su simplicidad y velocidad en consultas básicas. Resulta ideal para proyectos pequeños o medianos donde lo más importante es la rapidez de implementación y el soporte de la comunidad. Su curva de aprendizaje es baja, por lo que es una excelente opción si estás empezando en el mundo del backend.
  • PostgreSQL → Destaca por ser más avanzado y potente. Además de manejar datos relacionales, soporta estructuras como JSON y cuenta con funciones avanzadas que lo hacen ideal para aplicaciones grandes y complejas. Su capacidad para trabajar con datos no estructurados y su compatibilidad con transacciones complejas lo convierten en la elección favorita en muchos entornos profesionales.

👉 Para aprender a crear API REST, cualquiera de los dos es perfectamente válido. Lo importante es comprender la lógica de cómo se conecta el backend con la base de datos. Sin embargo, en producción, muchas empresas optan por PostgreSQL por su escalabilidad y robustez, mientras que otras siguen confiando en MySQL por su estabilidad y sencillez.


Testing con Jest y Supertest

Antes de publicar la API es recomendable probar los endpoints automáticamente. Con librerías como Jest o Supertest podemos ejecutar tests que verifican que cada ruta responde como debería. Así detectamos errores rápidamente y mantenemos la calidad del proyecto en el tiempo.

import request from "supertest";
import app from "../src/index";

describe("GET /health", () => {
  it("debería responder correctamente", async () => {
    const res = await request(app).get("/health");
    expect(res.status).toBe(200);
    expect(res.body.ok).toBe(true);
  });
});

👉 Esto asegura que la API siga funcionando después de cambios.


Consumo de la API desde Angular

Finalmente, toda API necesita un cliente. Un servicio Angular típico se encarga de llamar a los endpoints, recibir datos en formato JSON y mostrarlos en la interfaz de usuario. Esto demuestra cómo backend y frontend se complementan: el servidor gestiona la lógica y el almacenamiento, mientras que el cliente ofrece una experiencia fluida al usuario final.

@Injectable({ providedIn: 'root' })
export class TodoService {
  private apiUrl = 'http://localhost:3000/todos';
  constructor(private http: HttpClient) {}

  list(): Observable<any[]> {
    return this.http.get<any[]>(this.apiUrl);
  }

  create(title: string): Observable<any> {
    return this.http.post<any>(this.apiUrl, { title });
  }

  update(id: number, done: boolean): Observable<any> {
    return this.http.patch<any>(`${this.apiUrl}/${id}`, { done });
  }

  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

👉 Así el frontend Angular puede leer y mostrar datos fácilmente.


Errores comunes al crear API REST

Al crear API REST es muy fácil cometer ciertos errores que, aunque parecen pequeños, terminan afectando seriamente al rendimiento, la seguridad y la experiencia de los desarrolladores que consumen la API. Estos son algunos de los más frecuentes:

  • No validar datos → Si no se comprueba lo que envía el cliente, es cuestión de tiempo que la base de datos se llene de información incorrecta o directamente dañina. Por ejemplo, un campo que debería ser un número puede recibir texto, o un email puede no tener el formato adecuado. La validación en el backend es clave para mantener la integridad de los datos.
  • No proteger rutas → Una API sin autenticación ni autorización es un blanco fácil. Imagina un endpoint /deleteUser abierto al público: cualquiera podría eliminar cuentas sin restricción. Implementar sistemas como JWT o roles de usuario evita que información sensible caiga en manos equivocadas.
  • Respuestas inconsistentes → Si en un endpoint devuelves un objeto con { "id": 1, "name": "Juan" } y en otro un formato distinto como { "userId": 2, "username": "Ana" }, el frontend se vuelve un caos. Las respuestas deben ser uniformes y predecibles, usando siempre el mismo estilo de nombres y estructura de datos.
  • No manejar errores → Cuando ocurre un fallo y la API simplemente “se cae” sin enviar un mensaje claro, los clientes no saben qué hacer. Centralizar la gestión de errores permite responder con códigos HTTP adecuados (400, 404, 500) y mensajes útiles para los desarrolladores.
  • No documentar → Una API sin documentación es como un coche sin volante: nadie sabe cómo usarla. Herramientas como Swagger generan una documentación interactiva que facilita el trabajo en equipo y la colaboración entre frontend y backend. Además, mejora la mantenibilidad del proyecto a largo plazo.

Conclusión

Ahora ya sabes que crear API REST con Node.js y Express no se trata solo de programar un par de rutas y devolver datos en JSON. Detrás de una API moderna hay todo un ecosistema de buenas prácticas y herramientas: autenticación segura con JWT, validación estricta de datos, manejo centralizado de errores, documentación interactiva con Swagger, despliegue con Docker y pruebas automatizadas que aseguren la calidad del proyecto.

Con esta guía tienes una base sólida para desarrollar APIs profesionales, no solo pensadas para funcionar en un entorno local, sino diseñadas para ser robustas, seguras y escalables en producción. Si aplicas paso a paso lo que hemos visto, podrás construir servicios capaces de crecer con tu proyecto, sin importar si los consumes desde Angular, React o incluso desde una aplicación móvil.

Si quieres seguir ampliando tus conocimientos, te recomiendo revisar la documentación oficial de Node.js, donde encontrarás funcionalidades más avanzadas del entorno. También es muy útil profundizar en el uso de Docker, ya que se ha convertido en una herramienta estándar para desplegar APIs en entornos profesionales.

Y por último, si aún no has leído mi artículo sobre Qué es una API y cómo trabajar con ella desde cero, te recomiendo hacerlo. Ese texto te dará una visión más conceptual que, unida a esta guía práctica, te permitirá dominar el proceso completo de principio a fin.

Compartir:

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Tabla de contenidos

Más posts

Categorías

Contáctame

Escríbeme a través del formulario. Estoy encantado de ayudarte con diseño web, contenido visual, redes o cualquier duda.