En el desarrollo backend, combinar Node.js con TypeScript ofrece un entorno robusto y tipado, mientras que JWT permite gestionar la autenticación de forma stateless. En este tutorial crearemos una API RESTful completa, desde la configuración del proyecto hasta su despliegue con Docker.
Tabla de contenidos
- Arquitectura de la API
- Configuración del proyecto
- Implementación de rutas y controladores
- Seguridad con JWT
- Contenerización con Docker
Arquitectura de la API
Una arquitectura limpia separa responsabilidades en capas: router, controller, service y repository. Esta separación facilita pruebas unitarias y el mantenimiento a largo plazo.
En nuestro caso utilizaremos la siguiente estructura de carpetas:
src/
│ index.ts # punto de entrada
│ app.ts # configuración de Express
│
├─ routes/
│ user.routes.ts # definición de endpoints
│
├─ controllers/
│ user.controller.ts # lógica de petición
│
├─ services/
│ user.service.ts # reglas de negocio
│
└─ models/
user.model.ts # definición del modelo (ejemplo con Prisma)
Esta disposición sigue el patrón Node.js y es compatible con cualquier ORM o base de datos que prefieras.
Configuración del proyecto
Inicializamos un proyecto npm y añadimos dependencias esenciales:
npm init -y
npm install express cors helmet jsonwebtoken dotenv
npm install -D typescript ts-node-dev @types/express @types/node @types/jsonwebtoken
Crearemos el archivo tsconfig.json con una configuración mínima:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
El archivo principal src/index.ts levanta el servidor:
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import dotenv from 'dotenv';
import userRouter from './routes/user.routes';
dotenv.config();
const app = express();
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use('/api/users', userRouter);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Servidor corriendo en puerto ${PORT}`));
Implementación de rutas y controladores
Definimos un router básico en src/routes/user.routes.ts:
import { Router } from 'express';
import { getAllUsers, registerUser, loginUser } from '../controllers/user.controller';
const router = Router();
router.get('/', getAllUsers);
router.post('/register', registerUser);
router.post('/login', loginUser);
export default router;
Los controladores gestionan la lógica de cada endpoint:
import { Request, Response } from 'express';
import { createUser, findUserByEmail, getUsers } from '../services/user.service';
import jwt from 'jsonwebtoken';
export const getAllUsers = async (req: Request, res: Response) => {
const users = await getUsers();
res.json(users);
};
export const registerUser = async (req: Request, res: Response) => {
const { email, password } = req.body;
const user = await createUser(email, password);
res.status(201).json(user);
};
export const loginUser = async (req: Request, res: Response) => {
const { email, password } = req.body;
const user = await findUserByEmail(email);
if (!user || user.password !== password) {
return res.status(401).json({ message: 'Credenciales inválidas' });
}
const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET as string, { expiresIn: '1h' });
res.json({ token });
};
Seguridad con JWT
Para proteger rutas privadas crearemos un middleware de verificación:
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export const verifyToken = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ message: 'Token ausente' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET as string);
// @ts-ignore
req.user = payload;
next();
} catch (error) {
res.status(403).json({ message: 'Token inválido' });
}
};
Aplicamos el middleware a cualquier ruta que requiera autenticación:
router.get('/profile', verifyToken, getUserProfile);
El secreto se define en un archivo .env:
JWT_SECRET=mi_secreto_super_seguro
PORT=4000
Contenerización con Docker
Para garantizar que la API se ejecuta de forma idéntica en cualquier entorno, crearemos una imagen Docker:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
El docker-compose.yml permite levantar la API junto a una base de datos (ejemplo PostgreSQL):
version: '3.8'
services:
api:
build: .
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Con docker compose up --build tendrás la API disponible en http://localhost:3000.
Conclusión
Hemos cubierto todo el proceso para crear una API RESTful con Node.js, TypeScript, autenticación basada en JWT y despliegue mediante Docker. Aplicar una arquitectura modular y seguir las mejores prácticas de seguridad garantiza un backend mantenible y listo para escalar.