Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/midudev/jscamp/llms.txt

Use this file to discover all available pages before exploring further.

Este módulo construye una API REST de estilo producción para la aplicación DevJobs usando Express v5. La arquitectura sigue el patrón MVC (Models-Controllers-Routes), con validación de esquemas mediante Zod, middleware CORS configurable y módulos ESM a lo largo de todo el proyecto.

Lo que aprenderás

  • Express v5 — el nuevo comportamiento del router y manejo de errores async
  • Arquitectura MVC — separación en models/, controllers/ y routes/
  • Zod — validación de esquemas con safeParse, campos requeridos y opcionales
  • Middleware CORS — lista de orígenes permitidos personalizable
  • Módulos ESMimport/export en toda la aplicación ("type": "module")
  • Paginación — query params limit y offset en el modelo
  • JSON import assertions — cargar jobs.json directamente como módulo

Estructura del Proyecto

04-express/
├── app.js                  # Entry point: crea la app Express y registra middlewares
├── config.js               # Constantes globales (PORT, límites de paginación)
├── jobs.json               # Dataset de empleos en memoria
├── routes/
│   └── jobs.js             # Definición de rutas y middlewares de validación inline
├── controllers/
│   └── jobs.js             # Lógica de cada endpoint (llama al modelo)
├── models/
│   └── job.js              # Acceso a datos: filtrado, paginación y CRUD en memoria
├── schemas/
│   └── jobs.js             # Esquemas Zod para validar el body de las requests
├── middlewares/
│   └── cors.js             # Middleware CORS con lista de orígenes permitidos
└── package.json            # Dependencias: express ^5.2.1, cors ^2.8.5, zod ^4.3.5

Arrancar el Servidor

Entry point: app.js

import express from 'express'
import { jobsRouter } from './routes/jobs.js'
import { corsMiddleware } from './middlewares/cors.js'
import { DEFAULTS } from './config.js'

const PORT = process.env.PORT ?? DEFAULTS.PORT
const app = express()

app.use(corsMiddleware())
app.use(express.json())

app.use('/jobs', jobsRouter)

if (!process.env.NODE_ENV) {
  app.listen(PORT, () => {
    console.log(`Servidor levantado en http://localhost:${PORT}`)
  })
}

export default app
La condición if (!process.env.NODE_ENV) evita que el servidor arranque automáticamente durante los tests (donde NODE_ENV=test).

Comandos

1

Entra al directorio e instala dependencias

cd 04-express
npm install
2

Arranca el servidor

node --watch app.js

Rutas de la API

El archivo routes/jobs.js define todas las rutas del recurso /jobs y aplica los middlewares de validación antes de pasar el control al controller:
import { Router } from 'express'
import { JobController } from '../controllers/jobs.js'
import { validateJob, validatePartialJob } from '../schemas/jobs.js'

export const jobsRouter = Router()

function validateCreate(req, res, next) {
  const result = validateJob(req.body)
  if (result.success) {
    req.body = result.data // datos validados y limpios
    return next()
  }
  return res.status(400).json({ error: 'Invalid request', details: result.error.errors })
}

const validateUpdate = (req, res, next) => {
  const result = validatePartialJob(req.body)
  if (!result.success) {
    return res.status(400).json({ error: JSON.parse(result.error.message) })
  }
  req.body = result.data
  next()
}

jobsRouter.get('/',      JobController.getAll)
jobsRouter.get('/:id',   JobController.getId)
jobsRouter.post('/',     validateCreate, JobController.create)
jobsRouter.patch('/:id', validateUpdate, JobController.partialUpdate)
jobsRouter.put('/:id',   JobController.update)
jobsRouter.delete('/:id', JobController.delete)

Referencia de endpoints

GET /jobs

Lista todos los empleos con filtros opcionales por text, title, level y technology. Soporta paginación con ?limit= y ?offset=.

GET /jobs/:id

Devuelve un empleo por su UUID. Retorna 404 si no existe.

POST /jobs

Crea un nuevo empleo. El body pasa por el middleware validateCreate (validación Zod completa).

PATCH /jobs/:id

Actualización parcial de un empleo. El body pasa por validateUpdate (validación Zod con .partial()).

Controller: controllers/jobs.js

import { DEFAULTS } from '../config.js'
import { JobModel } from '../models/job.js'

export class JobController {
  static async getAll(req, res) {
    const {
      text, title, level, technology,
      limit = DEFAULTS.LIMIT_PAGINATION,
      offset = DEFAULTS.LIMIT_OFFSET
    } = req.query

    const jobs = await JobModel.getAll({ text, title, level, limit, technology, offset })

    return res.json({
      data: jobs,
      total: jobs.length,
      limit: Number(limit),
      offset: Number(offset)
    })
  }

  static async getId(req, res) {
    const { id } = req.params
    const job = await JobModel.getById(id)

    if (!job) return res.status(404).json({ error: 'Job not found' })
    return res.json(job)
  }

  static async create(req, res) {
    const { titulo, empresa, ubicacion, data } = req.body
    const newJob = await JobModel.create({ titulo, empresa, ubicacion, data })
    return res.status(201).json(newJob)
  }
}

Validación con Zod

El archivo schemas/jobs.js define el esquema completo de un empleo y exporta dos funciones de validación:
import * as z from 'zod'

const jobSchema = z.object({
  titulo: z
    .string({ error: 'El título es obligatorio' })
    .min(3, 'El título debe tener al menos 3 caracteres')
    .max(100, 'El título no puede exceder los 100 caracteres'),
  empresa:    z.string(),
  ubicacion:  z.string(),
  descripcion: z.string().optional(),
  data: z.object({
    technology: z.array(z.string()),
    modalidad:  z.string(),
    nivel:      z.string(),
  }),
})

export function validateJob(input) {
  return jobSchema.safeParse(input)
}

export function validatePartialJob(input) {
  return jobSchema.partial().safeParse(input)
}

Campos del esquema

titulo
string
required
Título del puesto. Mínimo 3 caracteres, máximo 100.
empresa
string
required
Nombre de la empresa que publica la oferta.
ubicacion
string
required
Ciudad, país o "Remote" si es trabajo remoto.
descripcion
string
Descripción larga del puesto. Campo opcional.
data.technology
string[]
required
Array de tecnologías requeridas, p. ej. ["React", "TypeScript"].
data.modalidad
string
required
Modalidad de trabajo: "remote", "onsite" o "hybrid".
data.nivel
string
required
Nivel de experiencia: "junior", "mid" o "senior".
validateJob valida el esquema completo (creación). validatePartialJob llama a .partial() sobre el mismo esquema, haciendo todos los campos opcionales (ideal para PATCH).

Middleware CORS

El archivo middlewares/cors.js envuelve la librería cors con una lista de orígenes permitida configurable:
import cors from 'cors'

const ACCEPTED_ORIGINS = [
  'http://localhost:3000',
  'http://localhost:1234',
  'https://midu.dev',
  'http://localhost:5173'
]

export const corsMiddleware = ({ acceptedOrigins = ACCEPTED_ORIGINS } = {}) => {
  return cors({
    origin: (origin, callback) => {
      if (acceptedOrigins.includes(origin) || !origin) {
        return callback(null, true)
      }
      return callback(new Error('Origen no permitido'))
    }
  })
}
La condición || !origin permite solicitudes sin cabecera Origin (como las que vienen de herramientas CLI como curl o Postman). Para personalizar los orígenes permitidos puedes pasar un array al instanciar el middleware:
app.use(corsMiddleware({ acceptedOrigins: ['https://mi-app.com'] }))

Dependencias

PaqueteVersiónDescripción
express^5.2.1Framework web HTTP para Node.js
cors^2.8.5Middleware para gestionar cabeceras CORS
zod^4.3.5Validación de esquemas con inferencia de tipos
Este módulo usa Express v5, no v4. En v5, los errores lanzados en handlers async se propagan automáticamente al middleware de error sin necesidad de try/catch o next(err). Asegúrate de no mezclar patrones de v4 al extender la aplicación.

Build docs developers (and LLMs) love