Una API REST para una red social minimalista construida con Express 5, Mongoose 9 y autenticación por sesión con cookie. Los usuarios pueden registrarse, iniciar sesión, publicar posts de texto, comentar las publicaciones de otros y consultar su perfil.
Al completar este lab, serás capaz de:
- Diseñar y conectar tres modelos de Mongoose con relaciones entre colecciones (
ref,populate,virtual). - Implementar autenticación stateful con
express-sessionyconnect-mongo(sesión persistida en MongoDB). - Escribir controladores asíncronos con manejo de errores centralizado a través de
next(error). - Aplicar un middleware de autenticación reutilizable que protege rutas de forma selectiva.
- Estructurar una API REST siguiendo las convenciones del proyecto:
*.controller.js,*.mid.js,*.model.js. - Ejecutar tests de integración con Jest y Supertest sobre una base de datos en memoria.
- Node.js >= 20
- MongoDB corriendo en local (
mongodb://127.0.0.1:27017) o una cadena de conexión a MongoDB Atlas - Postman (opcional, para probar los endpoints manualmente)
git clone <url-del-repositorio>
cd lab-express-social-api/apinpm installCopia el fichero de plantilla y rellena los valores:
cp .env.template .envAbre .env y ajusta cada variable:
| Variable | Descripción | Valor por defecto |
|---|---|---|
PORT |
Puerto en el que escucha el servidor | 3000 |
MONGODB_URI |
Cadena de conexión a MongoDB | mongodb://127.0.0.1:27017/social-api |
SESSION_SECRET |
Clave para firmar la cookie de sesión | super secret |
SESSION_SECURE |
true solo en producción con HTTPS |
false |
CORS_ORIGIN |
Origen permitido por CORS | http://localhost:5173 |
Importante: nunca subas
.enval repositorio. El fichero.gitignoreya lo excluye.
npm run devEl servidor usa node --watch (sin nodemon). Al arrancar verás en la consola:
{"level":30,"msg":"Connected to MongoDB"}
{"level":30,"msg":"Server listening at port 3000"}
api/
├── src/
│ ├── server.js # Punto de entrada: arranca el servidor HTTP
│ ├── app.js # Crea la app Express y registra middlewares
│ ├── controllers/
│ │ ├── index.js # Router principal: declara todas las rutas
│ │ ├── users.controller.js # Registro, login, logout, perfil
│ │ ├── posts.controller.js # CRUD de posts
│ │ └── comments.controller.js # Crear y eliminar comentarios
│ ├── middlewares/
│ │ ├── index.js # Exporta { errors }
│ │ ├── auth.mid.js # Middleware de autenticación por sesión
│ │ └── errors.mid.js # 404 catch-all y manejador global de errores
│ └── lib/
│ ├── config.js # Configuración centralizada con convict
│ ├── db.js # Conexión a MongoDB con Mongoose
│ ├── logger.js # Instancia de pino
│ ├── session.js # Configuración de express-session + MongoStore
│ ├── cors.js # Configuración de CORS
│ └── models/
│ ├── user.model.js # Esquema User con bcrypt y virtual posts
│ ├── post.model.js # Esquema Post con virtual comments
│ └── comment.model.js # Esquema Comment
├── tests/
│ ├── users.test.js # Tests de registro, login, logout y perfil
│ ├── posts.test.js # Tests de CRUD de posts
│ └── comments.test.js # Tests de comentarios
├── docs/
│ └── api.postman_collection.json # Colección Postman lista para importar
├── seeds.js # Script para poblar la base de datos con datos falsos
├── .env.template # Plantilla de variables de entorno
└── package.json
El orden es obligatorio:
pino-http → cors → express.json → session → rutas → 404 → globalHandler
| Campo | Tipo | Validaciones |
|---|---|---|
name |
String | required |
username |
String | required, unique, trim |
email |
String | required, trim, lowercase, formato email |
password |
String | required, mínimo 8 caracteres — hasheado con bcrypt (salt 10) |
posts |
Virtual | reverse-populate desde Post.author |
El modelo incluye un hook pre('save') que hashea la contraseña automáticamente cuando el campo se modifica, y un método de instancia checkPassword(plain) que compara con bcrypt.compare.
| Campo | Tipo | Validaciones |
|---|---|---|
title |
String | required, minLength: 3, maxLength: 200 |
body |
String | required, minLength: 1, maxLength: 1000 |
author |
ObjectId | ref: User, required |
comments |
Virtual | reverse-populate desde Comment.post |
| Campo | Tipo | Validaciones |
|---|---|---|
body |
String | required, minLength: 1, maxLength: 500 |
author |
ObjectId | ref: User, required |
post |
ObjectId | ref: Post, required |
Todos los modelos tienen timestamps: true y una transformación toJSON que expone id (string) y elimina _id, __v y password.
Todos los endpoints tienen el prefijo /api/v0. Los marcados con [AUTH] requieren una sesión activa (cookie connect.sid).
| Método | Ruta | Auth | Body | Respuesta |
|---|---|---|---|---|
POST |
/api/v0/users |
No | { name, username, email, password } |
201 — objeto User |
POST |
/api/v0/sessions |
No | { email, password } |
200 — objeto User |
DELETE |
/api/v0/sessions |
[AUTH] |
— | 204 |
GET |
/api/v0/users/me |
[AUTH] |
— | 200 — objeto User con posts[] populado |
| Método | Ruta | Auth | Body | Respuesta |
|---|---|---|---|---|
GET |
/api/v0/posts |
[AUTH] |
— | 200 — array de Posts con author populado |
POST |
/api/v0/posts |
[AUTH] |
{ title, body } |
201 — objeto Post con author populado |
GET |
/api/v0/posts/:id |
[AUTH] |
— | 200 — Post con author y comments[].author populados |
PATCH |
/api/v0/posts/:id |
[AUTH] |
{ title?, body? } |
200 — Post actualizado con author |
DELETE |
/api/v0/posts/:id |
[AUTH] |
— | 204 |
| Método | Ruta | Auth | Body | Respuesta |
|---|---|---|---|---|
POST |
/api/v0/posts/:id/comments |
[AUTH] |
{ body } |
201 — objeto Comment |
DELETE |
/api/v0/posts/:id/comments/:commentId |
[AUTH] |
— | 204 |
| Código | Situación |
|---|---|
400 |
Error de validación de Mongoose ({ message, errors: { campo: "mensaje" } }) |
401 |
Sin sesión activa o credenciales incorrectas |
404 |
Recurso no encontrado (ruta inexistente o ID inválido/inexistente) |
409 |
username ya registrado |
500 |
Error inesperado del servidor |
Esta es la rama de solución. A continuación se describe qué hace cada parte.
Comprueba req.session.userId. Si no existe, responde 401 "session not found". Si existe pero el usuario no está en la base de datos, responde 401 "session user not found". En caso contrario, carga el documento User y lo adjunta a req.user para que los controladores lo usen directamente.
create: verifica que elusernameno esté duplicado (409), luego crea el usuario conUser.create(req.body). El hookpre('save')hashea la contraseña antes de persistirla.login: busca el usuario poremail, comprueba la contraseña concheckPassword(), guardauser._idenreq.session.userIdy devuelve el documento User.logout: destruye la sesión conreq.session.destroy()y responde204.profile: consulta el usuario autenticado y popula el virtualposts.
list: devuelve todos los posts conauthorpopulado.create: inyectaauthor: req.user._id(el id viene del middleware, no del body) y populaauthorantes de responder.detail: populaauthory el virtualcommentscon su propioauthoranidado (populate anidado).update: usafindByIdAndUpdateconrunValidators: truepara ejecutar las validaciones del esquema en la actualización parcial.remove: elimina el post y responde204.
create: extraepostdereq.params.idyauthordereq.user._id. El cliente solo envíabody.remove: usareq.params.commentIdpara eliminar el comentario.
Distingue tres casos antes del manejador genérico:
mongoose.Error.ValidationError→400con el mapa{ campo: "mensaje" }.mongoose.Error.CastErroren_id→404 "Resource not found"(ID con formato inválido).- Cualquier otro error → usa
error.statussi existe, o500.
El script seeds.js usa @faker-js/faker para generar 10 usuarios, 30 posts (3 por usuario) y 90 comentarios (3 por post) con datos realistas. Borra la base de datos antes de insertar.
npm run seedsTodos los usuarios generados tienen la contraseña Password1!.
- Abre Postman.
- Haz clic en Import y selecciona
api/docs/api.postman_collection.json. - La colección contiene todas las peticiones organizadas por recurso con ejemplos de body.
- Empieza por POST /users para registrarte y luego POST /sessions para obtener la cookie de sesión. Postman la guarda automáticamente para las siguientes peticiones.
Registrar un usuario:
curl -s -X POST http://localhost:3000/api/v0/users \
-H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace","username":"ada","email":"ada@example.com","password":"password123"}' \
| jqIniciar sesión (guarda la cookie en cookies.txt):
curl -s -c cookies.txt -X POST http://localhost:3000/api/v0/sessions \
-H "Content-Type: application/json" \
-d '{"email":"ada@example.com","password":"password123"}' \
| jqVer el feed de posts:
curl -s -b cookies.txt http://localhost:3000/api/v0/posts | jqCrear un post:
curl -s -b cookies.txt -X POST http://localhost:3000/api/v0/posts \
-H "Content-Type: application/json" \
-d '{"title":"Mi primer post","body":"Hola desde la Social API!"}' \
| jqVer el detalle de un post (sustituye <id> por un ID real):
curl -s -b cookies.txt http://localhost:3000/api/v0/posts/<id> | jqAñadir un comentario:
curl -s -b cookies.txt -X POST http://localhost:3000/api/v0/posts/<id>/comments \
-H "Content-Type: application/json" \
-d '{"body":"Gran post!"}' \
| jqVer el perfil propio:
curl -s -b cookies.txt http://localhost:3000/api/v0/users/me | jqCerrar sesión:
curl -s -b cookies.txt -c cookies.txt -X DELETE http://localhost:3000/api/v0/sessionsEl proyecto usa Jest con Supertest y mongodb-memory-server. La base de datos en memoria se levanta y limpia automáticamente en cada test, por lo que no es necesario tener MongoDB corriendo para ejecutar los tests.
npm testLos tests cubren:
- Registro de usuario: respuesta
201, ausencia depassworden la respuesta, conflicto409porusernameduplicado. - Login y logout:
200con credenciales válidas,401con contraseña incorrecta,204al cerrar sesión. - Perfil:
401sin sesión,200con sesión válida y arraypostspopulado. - CRUD de posts:
200/201/204en cada operación,401sin sesión,404para IDs inexistentes. - Comentarios:
201al crear,204al eliminar,404para comentarios inexistentes.
- Los tres modelos (
User,Post,Comment) están definidos con los campos y validaciones correctos. - El modelo
Userincluye hookpre('save')con bcrypt y métodocheckPassword(). - Los modelos
UseryPosttienen los virtuales de reverse-populate correctamente configurados. - Todos los modelos tienen
timestamps: truey la transformacióntoJSONque oculta_id,__vypassword.
-
POST /api/v0/userscrea el usuario con la contraseña hasheada y devuelve201. -
POST /api/v0/sessionsvalida credenciales, guardauserIden sesión y devuelve el usuario sinpassword. -
DELETE /api/v0/sessionsdestruye la sesión y devuelve204. -
GET /api/v0/users/medevuelve el usuario autenticado conposts[]populado.
-
GET /api/v0/postsdevuelve todos los posts conauthorpopulado (requiere sesión). -
POST /api/v0/postsasignaauthordesdereq.user._id, no desde el body (requiere sesión). -
GET /api/v0/posts/:iddevuelve el post conauthorycomments[].authorpopulados (requiere sesión). -
PATCH /api/v0/posts/:idactualiza el post conrunValidators: true(requiere sesión). -
DELETE /api/v0/posts/:idelimina el post y devuelve204(requiere sesión).
-
POST /api/v0/posts/:id/commentscrea el comentario conauthorypostdesde la sesión/params (requiere sesión). -
DELETE /api/v0/posts/:id/comments/:commentIdelimina el comentario y devuelve204(requiere sesión).
- El middleware
auth.mid.jses reutilizable y se aplica por ruta, no globalmente. - Todos los controladores usan el patrón
try/catchconnext(error). - El manejador global de errores distingue
ValidationError(400),CastError(404) y errores genéricos (500). - No se accede a
process.envdirectamente fuera delib/config.js. - Todos los tests pasan con
npm test.
Happy coding!