Reto opcional de construcción de una API REST de gestión de usuarios.
Este proyecto tiene como objetivo construir paso a paso una API REST capaz de gestionar usuarios, autenticación, roles, seguridad, base de datos e integración con un frontend.
Instalar dependencias:
npm installArrancar en modo desarrollo:
npm run devLa API se ejecutará inicialmente en:
http://localhost:3000
GET /api/healthRespuesta esperada:
{
"status": "ok",
"message": "UserManager API funcionando",
"timestamp": "2026-01-01T10:00:00.000Z"
}GET /api/users
GET /api/users/:id
POST /api/users
PATCH /api/users/:id
DELETE /api/users/:idEstos endpoints todavía no trabajan con datos reales. De momento sirven para practicar métodos HTTP, rutas, parámetros y body.
Estas rutas se han creado para practicar cómo leer datos de una petición HTTP.
POST /api/debug/body
GET /api/debug/params/:id
GET /api/debug/query
GET /api/debug/headers
PATCH /api/debug/users/:idMás adelante estas rutas podrán eliminarse, ya que no forman parte de la API final.
GET /api/usersDevuelve el listado de usuarios cargados en memoria.
Respuesta de ejemplo:
{
"message": "Listado de usuarios",
"total": 3,
"data": []
}GET /api/users
GET /api/users/:idDevuelve un usuario concreto a partir de su ID.
Respuesta correcta:
{
"message": "Usuario encontrado",
"data": {
"id": 1,
"name": "Ana García",
"email": "ana@email.com",
"role": "USER",
"isActive": true
}
}Posibles errores:
{
"error": "El ID debe ser un número"
}{
"error": "Usuario no encontrado"
}POST /api/usersBody:
{
"name": "María López",
"email": "maria@email.com",
"password": "123456"
}Respuesta correcta:
{
"message": "Usuario creado correctamente",
"data": {
"id": 4,
"name": "María López",
"email": "maria@email.com",
"role": "USER",
"isActive": true
}
}Posibles errores:
{
"error": "name, email y password son obligatorios"
}{
"error": "La contraseña debe tener al menos 6 caracteres"
}{
"error": "El email ya está registrado"
}PATCH /api/users/:idPermite modificar parcialmente los datos de un usuario.
Campos permitidos:
name
email
isActive
Body de ejemplo:
{
"name": "Ana Martínez"
}Respuesta correcta:
{
"message": "Usuario actualizado correctamente",
"data": {
"id": 1,
"name": "Ana Martínez",
"email": "ana@email.com",
"role": "USER",
"isActive": true
}
}Posibles errores:
{
"error": "El ID debe ser un número",
"received": "abc"
}{
"error": "Usuario no encontrado",
"id": 999
}{
"error": "Debes enviar al menos un campo para actualizar"
}{
"error": "El email ya está registrado"
}DELETE /api/users/:idEn este proyecto, esta ruta no borra físicamente el usuario. Realiza un borrado lógico marcando:
isActive = false
Respuesta correcta:
{
"message": "Usuario desactivado correctamente",
"data": {
"id": 1,
"name": "Ana García",
"email": "ana@email.com",
"role": "USER",
"isActive": false
}
}Posibles errores:
{
"error": "El ID debe ser un número",
"received": "abc"
}{
"error": "Usuario no encontrado",
"id": 999
}La API realiza validaciones manuales antes de crear o actualizar usuarios.
Validaciones principales:
namedebe ser un texto no vacío.emaildebe ser un texto no vacío.passworddebe ser un texto no vacío.passworddebe tener al menos 6 caracteres.emaildebe contener@.isActivedebe ser boolean.
Ejemplo de error:
{
"error": "El nombre debe ser un texto no vacío"
}La API normaliza los emails antes de guardarlos o compararlos.
Proceso aplicado:
trim()toLowerCase()- Validación básica de formato.
- Comprobación de duplicados.
Ejemplo:
" USUARIO@EMAIL.COM " -> "usuario@email.com"
Si se intenta crear o actualizar un usuario con un email ya existente, la API responde:
{
"error": "El email ya está registrado"
}Código:
409 ConflictLa API utiliza códigos HTTP para indicar el resultado de cada petición.
| Código | Significado | Uso en el proyecto |
|---|---|---|
| 200 | OK | Consulta, actualización o desactivación correcta |
| 201 | Created | Usuario creado correctamente |
| 400 | Bad Request | Datos incorrectos o incompletos |
| 404 | Not Found | Usuario no encontrado |
| 409 | Conflict | Email duplicado |
Ejemplo de error 404:
{
"error": "Usuario no encontrado",
"id": 999
}Ejemplo de error 409:
{
"error": "El email ya está registrado"
}La API utiliza un middleware global para devolver errores con un formato común.
Formato general:
{
"error": "Mensaje del error",
"statusCode": 400,
"details": {},
"path": "/api/users/abc",
"method": "GET",
"timestamp": "2026-01-01T10:00:00.000Z"
}También se ha añadido un middleware para rutas no encontradas:
GET /api/ruta-inventadaRespuesta:
{
"error": "Ruta no encontrada",
"statusCode": 404
}Hasta el día 15, la API trabaja con usuarios en memoria.
Esto significa que los datos se pierden al reiniciar el servidor.
A partir de la siguiente fase, prepararemos una base de datos para guardar los usuarios de forma persistente.
Tabla principal prevista:
users
Campos principales:
id
name
email
password_hash
role
is_active
created_at
updated_at
El proyecto utiliza Docker Compose para levantar PostgreSQL y Adminer.
Servicios:
postgres -> Base de datos PostgreSQL
adminer -> Interfaz web para consultar la base de datos
Comando para arrancar:
docker compose up -dComando para parar:
docker compose downAdminer:
http://localhost:8080
Datos de conexión:
Sistema: PostgreSQL
Servidor: postgres
Usuario: usermanager
Contraseña: usermanager_password
Base de datos: usermanager_db
El modelo principal del proyecto será User.
Campos principales:
id
name
email
passwordHash
role
isActive
createdAt
updatedAt
Reglas importantes:
email único
passwordHash nunca se devuelve
role por defecto USER
isActive por defecto true
createdAt y updatedAt automáticos
Este diseño se convertirá más adelante en un modelo Prisma.
El proyecto usará Prisma como ORM principal para comunicarse con PostgreSQL.
Se ha elegido Prisma porque:
Encaja bien con TypeScript.
Permite definir modelos claros.
Incluye migraciones.
Genera un cliente tipado.
Permite explorar datos con Prisma Studio.
Flujo previsto:
API Express → Repository → Prisma → PostgreSQL
SQL directo, TypeORM y Sequelize se han considerado como alternativas, pero no serán el camino principal del reto.
El proyecto utilizará Prisma como ORM principal para comunicarse con PostgreSQL.
Instalación:
npm install -D prisma
npm install @prisma/clientInicialización:
npx prisma init --datasource-provider postgresqlArchivos importantes:
prisma/schema.prisma
.env
.env.example
Validar esquema:
npx prisma validateGenerar cliente:
npx prisma generateEl modelo principal del proyecto será User.
enum Role {
USER
ADMIN
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
passwordHash String
role Role @default(USER)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Reglas principales:
email único
passwordHash obligatorio
role por defecto USER
isActive por defecto true
createdAt automático
updatedAt automático al modificar
El proyecto usa Prisma Migrate para versionar la estructura de la base de datos.
Primera migración:
npx prisma migrate dev --name initEsto genera:
prisma/migrations/<timestamp>_init/migration.sql
Y crea en PostgreSQL:
User
_prisma_migrations
La tabla User almacena los usuarios de la aplicación.
La tabla _prisma_migrations guarda el historial interno de migraciones de Prisma.
Prisma Studio permite explorar visualmente los datos de la base de datos.
Comando:
npx prisma studioO mediante script:
npm run prisma:studioURL habitual:
http://localhost:5555
Uso en el proyecto:
Comprobar tablas.
Revisar usuarios.
Ver datos iniciales del seed.
Comprobar cambios realizados desde la API.
Detectar errores de persistencia.
Prisma Studio es una herramienta de desarrollo. La gestión real de usuarios se hará desde la API.
El proyecto incluye un seed para crear usuarios iniciales.
Archivo:
prisma/seed.ts
Ejecutar seed:
npx prisma db seedO mediante script:
npm run prisma:seedUsuarios iniciales:
| Role | Estado | |
|---|---|---|
admin@email.com |
ADMIN |
activo |
user@email.com |
USER |
activo |
inactive@email.com |
USER |
inactivo |
Nota:
Los passwordHash son temporales hasta implementar bcrypt en la fase de seguridad.
La API ya puede consultar usuarios desde PostgreSQL usando Prisma Client.
Archivo de cliente compartido:
src/prisma.ts
Este proyecto usa Prisma 7 con adapter PostgreSQL:
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "./generated/prisma/client";Rutas temporales de prueba:
| Método | Ruta | Acción |
|---|---|---|
| GET | /api/debug/prisma/users |
Listar usuarios |
| GET | /api/debug/prisma/users-active |
Listar usuarios activos |
| GET | /api/debug/prisma/users/:id |
Buscar usuario por ID |
| POST | /api/debug/prisma/users |
Crear usuario |
Regla:
Las respuestas no deben incluir passwordHash.
El proyecto empieza a organizarse por capas.
Primera carpeta creada:
src/routes/
Archivos actuales:
src/routes/health.routes.ts
src/routes/debug-prisma.routes.ts
server.ts monta los routers:
app.use("/api/health", healthRouter);
app.use("/api/debug/prisma", debugPrismaRouter);Esta separación permite que server.ts quede más limpio y que el proyecto pueda crecer hacia una arquitectura con controladores, servicios y repositorios.
El proyecto empieza a separar la lógica HTTP en controladores.
Carpeta creada:
src/controllers/
Archivos actuales:
src/controllers/health.controller.ts
src/controllers/user.controller.ts
Ejemplo de ruta simplificada:
debugPrismaRouter.get("/users", getUsers);La lógica de la petición queda en el controlador:
getUsers
getUserById
createDebugUser
Esta separación prepara el proyecto para añadir servicios y repositorios.
El proyecto ya incluye una capa de servicios.
Carpeta creada:
src/services/
Archivo principal:
src/services/user.service.ts
Los servicios contienen lógica de negocio como:
- Validar datos.
- Normalizar email.
- Comprobar usuario inexistente.
- Gestionar email duplicado.
- Crear usuarios.
El controlador queda más limpio y llama a funciones como:
getUsersService()
getUserByIdService(id)
createDebugUserService(req.body)En este punto, el servicio todavía usa Prisma directamente. En el siguiente paso se añadirá una capa de repositorios.
El proyecto ya incluye una capa de repositorios.
Carpeta creada:
src/repositories/
Archivo principal:
src/repositories/user.repository.ts
Funciones actuales:
findAllUsersfindActiveUsersfindUserByIdfindUserByEmailcreateUser
Flujo actual de la API: Route → Controller → Service → Repository → Prisma → PostgreSQL.
El servicio ya no usa Prisma directamente. Ahora el acceso a datos queda concentrado en el repositorio.
La API ya tiene rutas reales para gestionar usuarios con PostgreSQL y Prisma.
Rutas principales:
| Método | Ruta | Acción |
|---|---|---|
| GET | /api/users |
Listar usuarios |
| GET | /api/users/:id |
Consultar usuario |
| POST | /api/users |
Crear usuario |
| PATCH | /api/users/:id |
Actualizar usuario |
| DELETE | /api/users/:id |
Desactivar usuario |
Flujo interno:
Route → Controller → Service → Repository → Prisma → PostgreSQL
El borrado es lógico:
DELETE /api/users/:id → isActive = false
La API nunca devuelve passwordHash.
El proyecto ya incluye una primera ruta de autenticación para registro de usuarios.
Ruta:
POST /api/auth/register
Body esperado:
{
"name": "Usuario Nuevo",
"email": "nuevo@email.com",
"password": "123456"
}Respuesta correcta:
201 Created.
Reglas:
- El email no puede estar repetido.
- La contraseña se guarda como
passwordHashusandobcrypt. - El usuario se registra con
role USERpor defecto. - El usuario se registra activo por defecto.
passwordHashnunca se devuelve al cliente.
Todavía no se genera token JWT. Eso se añadirá más adelante.
Ruta:
POST /api/auth/login
Body esperado:
{
"email": "user@email.com",
"password": "user123"
}Respuesta correcta:
200 OK
Respuesta aproximada:
{
"message": "Login correcto",
"data": {
"user": {
"id": 2,
"name": "Usuario Demo",
"email": "user@email.com",
"role": "USER",
"isActive": true
}
}
}Reglas:
- El email debe existir.
- La contraseña debe coincidir con el
passwordHash. - El usuario debe estar activo.
passwordHashnunca se devuelve.- Todavía no se devuelve token JWT.
Ruta:
POST /api/auth/login
Body esperado:
{
"email": "user@email.com",
"password": "user123"
}Respuesta correcta:
{
"message": "Login correcto",
"data": {
"user": {
"id": 2,
"name": "Usuario Demo",
"email": "user@email.com",
"role": "USER",
"isActive": true
},
"token": "eyJhbGciOiJIUzI1NiIs..."
}
}El token es un JWT firmado con JWT_SECRET.
Variables necesarias:
JWT_SECRET="cambia_esta_clave_en_produccion"
JWT_EXPIRES_IN="1h"Reglas:
- El token se genera solo si el login es correcto.
- El token contiene
userId,emailyrole. - El token no contiene
passwordnipasswordHash. - Todavía no se usa para proteger rutas.
El proyecto ya puede verificar tokens JWT enviados por el cliente.
Formato de la cabecera:
Authorization: Bearer <token>
Middleware creado:
src/middlewares/auth.middleware.ts
El middleware:
- Lee la cabecera
Authorization. - Comprueba que el formato sea
Bearer. - Verifica el token con
JWT_SECRET. - Guarda los datos autenticados en
req.user. - Bloquea la petición si el token falta o es inválido.
Ruta de prueba:
GET /api/auth/me
Rutas protegidas:
/api/users/*
Todavía no se aplican permisos por rol. Eso se trabajará en el siguiente paso.
El proyecto distingue entre dos roles:
USER
ADMIN
Reglas principales:
| Ruta | Permiso |
|---|---|
GET /api/users |
Solo ADMIN |
POST /api/users |
Solo ADMIN |
GET /api/users/me |
Usuario autenticado |
GET /api/users/:id |
ADMIN o el propio usuario |
PATCH /api/users/:id |
ADMIN o el propio usuario |
DELETE /api/users/:id |
Solo ADMIN |
Middlewares creados:
requireRole
requireSelfOrAdmin
Códigos importantes:
401 → No autenticado
403 → Autenticado, pero sin permiso
src/
├── controllers/ # Gestión de peticiones HTTP y respuestas
│ ├── auth.controller.ts
│ ├── health.controller.ts
│ └── user.controller.ts
├── errors/ # Manejo centralizado de excepciones
│ └── AppError.ts
├── middlewares/ # Interceptores de autenticación y autorización
│ ├── auth.middleware.ts
│ └── role.middleware.ts
├── repositories/ # Acceso a base de datos con Prisma
│ └── user.repository.ts
├── routes/ # Enrutamiento de endpoints
│ ├── auth.routes.ts
│ ├── health.routes.ts
│ └── user.routes.ts
├── services/ # Lógica de negocio y validaciones
│ ├── auth.service.ts
│ └── user.service.ts
├── types/ # Tipos e interfaces globales
│ └── auth.types.ts
├── utils/ # Funciones auxiliares y helpers
│ ├── jwt.utils.ts
│ ├── parse.utils.ts
│ ├── password.utils.ts
│ └── string.utils.ts
├── prisma.ts # Cliente de conexión Prisma
└── server.ts # Servidor Express principal
- Día 1 - Diseño inicial
- Día 2 - Preparación del proyecto
- Día 3 - Primer endpoint
- Día 4 - Métodos HTTP
- Día 5 - JSON, body, params y headers
- Día 6 - Cliente HTTP y depuración
- Día 7 - Listado de usuarios en memoria
- Día 8 - Consultar usuario por ID
- Día 9 - Crear usuarios en memoria
- Día 10 - Actualizar usuarios en memoria
- Día 11 - Eliminar o desactivar usuarios en memoria
- Día 12 - Validación manual básica
- Día 13 - Validación de email y duplicados
- Día 14 - Códigos de estado HTTP
- Día 15 - Middleware centralizado de errores
- Día 16 - Base de datos y persistencia
- Día 17 - PostgreSQL con Docker Compose
- Día 18 - Diseño del modelo persistente User
- Día 19 - ORM o acceso a datos
- Día 20 - Instalación y configuración inicial de Prisma
- Día 21 - Modelo Prisma User
- Día 22 - Primera migración con Prisma
- Día 23 - Prisma Studio
- Día 24 - Seed de datos iniciales
- Día 25 - Consultas básicas con Prisma Client
- Día 26 - Separar rutas
- Día 27 - Controladores
- Día 28 - Servicios
- Día 29 - Repositorio con Prisma
- Día 30 - CRUD persistente ordenado
- Día 31 - Limpieza y refactor
- Día 32 - Contraseñas seguras con bcrypt
- Día 33 - Registro de usuarios
- Día 34 - Login de usuarios
- Día 35 - Generación de token JWT
- Día 36 - Middleware de autenticación
- Día 37 - Roles y permisos
- Día 38 - Conexión con el frontend
- Día 39 - Pruebas de integración
- Día 40 - Revisión final del proyecto