Node trae un servidor HTTP de serie, node:http, y con @types/node TypeScript conoce todos sus tipos. Montamos una API de tareas sin ningún framework. Los tipos ayudan en dos frentes: que las rutas estén bien escritas y que lo que llega del cliente se compruebe antes de usarlo.
También te puede interesar
El servidor
Instala los tipos de Node con npm install -D @types/node; usamos la versión 24.19.2. El código:
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
interface Tarea {
id: number;
titulo: string;
hecha: boolean;
}
const tareas: Tarea[] = [
{ id: 1, titulo: "Comprar café", hecha: false },
{ id: 2, titulo: "Revisar el correo", hecha: true },
];
// Lo único que aceptamos al crear una tarea.
type NuevaTarea = Pick<Tarea, "titulo">;
function esNuevaTarea(x: unknown): x is NuevaTarea {
return typeof x === "object" && x !== null && "titulo" in x &&
typeof x.titulo === "string" && x.titulo.trim() !== "";
}
function responder(res: ServerResponse, estado: number, cuerpo: unknown): void {
res.writeHead(estado, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(cuerpo));
}
async function leerCuerpo(req: IncomingMessage): Promise<unknown> {
let texto = "";
for await (const trozo of req) texto += trozo;
return JSON.parse(texto);
}
// Cada ruta es «MÉTODO /ruta» y su manejador. El tipo impide registrar un método inventado.
type Metodo = "GET" | "POST" | "DELETE";
type Manejador = (req: IncomingMessage, res: ServerResponse, id?: number) => Promise<void> | void;
const rutas: Record<`${Metodo} ${string}`, Manejador> = {
"GET /tareas": (_req, res) => responder(res, 200, tareas),
"POST /tareas": async (req, res) => {
let datos: unknown;
try {
datos = await leerCuerpo(req);
} catch {
return responder(res, 400, { error: "El cuerpo no es JSON válido" });
}
if (!esNuevaTarea(datos)) return responder(res, 422, { error: "Falta «titulo» (texto)" });
const nueva: Tarea = { id: tareas.length + 1, titulo: datos.titulo.trim(), hecha: false };
tareas.push(nueva);
responder(res, 201, nueva);
},
"DELETE /tareas/:id": (_req, res, id) => {
const i = tareas.findIndex((t) => t.id === id);
if (i === -1) return responder(res, 404, { error: `No hay ninguna tarea ${id}` });
const [borrada] = tareas.splice(i, 1);
responder(res, 200, borrada);
},
};
const servidor = createServer(async (req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
const m = url.pathname.match(/^\/tareas\/(\d+)$/);
const clave = m ? `${req.method} /tareas/:id` : `${req.method} ${url.pathname}`;
const manejador = rutas[clave as keyof typeof rutas];
if (!manejador) return responder(res, 404, { error: `No existe ${clave}` });
await manejador(req, res, m ? Number(m[1]) : undefined);
});
servidor.listen(3000, () => console.log("Escuchando en http://localhost:3000"));npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es servidor.ts
tsc(sin errores)
node servidor.ts
salidaEscuchando en http://localhost:3000
Las piezas con tipos
| Pieza | Qué aporta |
|---|---|
type IncomingMessage, type ServerResponse | Los tipos de la petición y la respuesta de Node. Se importan con type porque solo se usan como tipos. |
Record<`${Metodo} ${string}`, Manejador> | Las claves de la tabla de rutas tienen que empezar por GET, POST o DELETE. Es un tipo de plantilla de texto. |
esNuevaTarea(x: unknown): x is NuevaTarea | Una guarda de tipo: comprueba el cuerpo y, si devuelve true, TypeScript lo trata como NuevaTarea. |
leerCuerpo(): Promise<unknown> | Lo que manda el cliente es unknown hasta que se compruebe, no any. |
Pick<Tarea, "titulo"> | Lo único que se acepta al crear: el id y hecha los pone el servidor. |
La tabla de rutas no admite un método que no exista. Si alguien añade PATCH sin ampliar el tipo Metodo:
import type { IncomingMessage, ServerResponse } from "node:http";
type Metodo = "GET" | "POST" | "DELETE";
type Manejador = (req: IncomingMessage, res: ServerResponse) => void;
const rutas: Record<`${Metodo} ${string}`, Manejador> = {
"GET /tareas": () => {},
"PATCH /tareas": () => {},
};npx tsc --noEmit --strict --target esnext --module nodenext --types node --locale es metodo-error.ts
tscmetodo-error.ts(6,3): error TS2353: El literal de objeto solo puede especificar propiedades conocidas y '"PATCH /tareas"' no existe en el tipo 'Record<`DELETE ${string}` | `GET ${string}` | `POST ${string}`, Manejador>'.Probarlo
Un cliente que recorre todas las rutas, incluidos los casos malos: un cuerpo sin titulo, uno que no es JSON, borrar algo que no existe y un método sin ruta.
// Un cliente que usa la API del servidor y comprueba cada respuesta.
const BASE = "http://localhost:3000";
async function pedir(metodo: string, ruta: string, cuerpo?: string): Promise<void> {
const res = await fetch(BASE + ruta, {
method: metodo,
headers: { "Content-Type": "application/json" },
body: cuerpo,
});
console.log(`${metodo} ${ruta} -> ${res.status}`, await res.text());
}
await pedir("GET", "/tareas");
await pedir("POST", "/tareas", JSON.stringify({ titulo: "Llamar al banco" }));
await pedir("POST", "/tareas", JSON.stringify({ nombre: "sin título" }));
await pedir("POST", "/tareas", "{esto no es JSON");
await pedir("DELETE", "/tareas/2");
await pedir("DELETE", "/tareas/99");
await pedir("PATCH", "/tareas");
await pedir("GET", "/tareas");node cliente.ts
salidaGET /tareas -> 200 [{"id":1,"titulo":"Comprar café","hecha":false},{"id":2,"titulo":"Revisar el correo","hecha":true}]
POST /tareas -> 201 {"id":3,"titulo":"Llamar al banco","hecha":false}
POST /tareas -> 422 {"error":"Falta «titulo» (texto)"}
POST /tareas -> 400 {"error":"El cuerpo no es JSON válido"}
DELETE /tareas/2 -> 200 {"id":2,"titulo":"Revisar el correo","hecha":true}
DELETE /tareas/99 -> 404 {"error":"No hay ninguna tarea 99"}
PATCH /tareas -> 404 {"error":"No existe PATCH /tareas"}
GET /tareas -> 200 [{"id":1,"titulo":"Comprar café","hecha":false},{"id":3,"titulo":"Llamar al banco","hecha":false}]| Código | Cuándo lo devuelve |
|---|---|
| 200 | Todo bien: lista o tarea borrada |
| 201 | Tarea creada |
| 400 | El cuerpo no es JSON |
| 404 | No existe la tarea o la ruta |
| 422 | Es JSON, pero no trae lo que hace falta |
unknown y pasa por esNuevaTarea. Para APIs más grandes, un esquema de Zod hace lo mismo con menos código.Todo el código de esta página se comprobó con TypeScript 7.0.2 en modo estricto (--strict --locale es) y se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; los mensajes de error y las salidas están copiados de esa ejecución. Documentación oficial: Módulo node:http de Node.js.