Skip to main content
The backend is a Node.js application in the back/ directory. It exposes a JSON REST API under the /api/v1 prefix and a Swagger UI at /api-docs.

Tech stack

TechnologyVersionRole
Node.js20Runtime
Express5HTTP framework
PostgreSQL16Database
pgDirect SQL queries
Knex.js3Migrations and seeders only
Argon2idPassword hashing
JSON Web TokensAuthentication tokens
WinstonStructured logging
Swagger / swagger-autogenAPI documentation

Module structure

The src/modules/ directory is organized by domain, not by layer. Every domain module contains five files with consistent naming.
src/
├── app.js                        # Express app entry point
├── config/
│   ├── db.js                     # pg connection pool
│   ├── logger.js                 # Winston logger instance
│   └── swagger-output.json       # Auto-generated Swagger spec
├── midlewares/
│   ├── auth.js                   # authenticateToken
│   ├── roles.js                  # authorizeRole
│   ├── logger.midleware.js       # httpLogger
│   ├── error.midleware.js        # Global error handler
│   └── validator.js              # Request body validation runner
├── modules/
│   ├── index.js                  # Aggregates all domain routers
│   ├── auth/
│   │   ├── auth.controller.js
│   │   ├── auth.service.js
│   │   ├── auth.repository.js
│   │   ├── auth.routes.js
│   │   └── auth.validator.js
│   ├── usuarios/
│   │   ├── usuarios.controller.js
│   │   ├── usuarios.service.js
│   │   ├── usuarios.repository.js
│   │   ├── usuarios.routes.js
│   │   └── usuarios.validator.js
│   ├── proyectos/
│   │   ├── proyectos.controller.js
│   │   ├── proyectos.service.js
│   │   ├── proyectos.repository.js
│   │   ├── proyectos.routes.js
│   │   └── proyectos.validator.js
│   └── tareas/
│       ├── tareas.controller.js
│       ├── tareas.services.js
│       ├── tareas.repository.js
│       ├── tareas.routes.js
│       └── tareas.validator.js
└── utils/
    ├── hash.js                   # Argon2id helpers
    ├── jwt.js                    # createJWT
    └── response.js               # successResponse / errorResponse

Design patterns

Repository pattern

Every domain module contains a .repository.js file that is the only place in the codebase where SQL is written. Repositories accept a pg client, execute a query, and return rows. Services never touch the database directly. This decouples business logic from data access: changing a query or adding a database index does not require touching service or controller code.

Service layer

Services contain domain business rules and are the only layer that enforces them. Examples:
  • Verifying that a PM is the owner_id of a project before allowing archive or edit operations.
  • Checking that a user is active (activo = true) before issuing a JWT.
  • Orchestrating multiple repository calls inside a single pg transaction.
Controllers call exactly one service function per endpoint and delegate all decisions to it.

MVC adapted

Controllers handle the HTTP cycle only. They parse the request, call the service, and format the response with successResponse or errorResponse from utils/response.js. No conditional logic or business rules appear in controllers.
// Typical controller shape
export const login = async (req, res) => {
  const { email, password } = req.body
  const result = await loginService(email, password)
  return successResponse(res, 200, 'Login exitoso', result)
}

Middleware pipeline

All routes are mounted under /api/v1 in app.js. Protected routes pass through three middleware functions in order:
Request


httpLogger          — logs method, URL, status code, and response time


authenticateToken   — verifies JWT; attaches req.usuario = { id, rol }
  │                    returns 401 if missing/expired, 403 if invalid

authorizeRole([…])  — queries USERS table to confirm current role
  │                    returns 403 if role not in allowed array

Controller
httpLogger is applied globally in app.js. authenticateToken and authorizeRole are applied per-router in each module’s .routes.js file, so public routes (e.g. POST /login, POST /register) skip authentication entirely.

httpLogger

Logging is implemented as a response-finish hook rather than request middleware, so the log line includes the final HTTP status code and elapsed milliseconds:
logger[level](`${req.method} ${req.url} ${res.statusCode} - ${ms}ms`)
The log level is determined by status code: info for 2xx/3xx, warn for 4xx, error for 5xx.

authenticateToken

Extracts the Bearer token from the Authorization header, verifies it with jwt.verify(token, process.env.TOKEN), and attaches the decoded payload as req.usuario. Distinguishes between expired tokens (401) and invalid tokens (403).

authorizeRole

Receives an array of permitted roles and returns an async middleware. It queries the database for the user’s current role rather than trusting the role in the JWT payload, which means role changes take effect on the next request without requiring a new token.
export const authorizeRole = (rolesPermitidos) => {
  return async (req, res, next) => {
    const { rows } = await pool.query(
      'SELECT ROL FROM USERS WHERE ID_USUARIO=$1',
      [req.usuario.id]
    )
    const rol = rows[0]?.rol
    if (!rol || !rolesPermitidos.includes(rol)) {
      return res.status(403).json({ mensaje: 'No tienes permiso para esta acción' })
    }
    req.usuario.rol = rol
    next()
  }
}

Database access: pg over ORM

Repositories use the pg connection pool directly — there is no ORM such as Sequelize or Prisma. SQL is written explicitly in each repository function. Knex.js is present in the project but is used only for migrations and seeders.
Using direct SQL gives complete control over query shape and execution plans. If you add a new query, write it in the relevant .repository.js file. Do not use the Knex query builder inside application code.

Knex.js configuration

knexfile.js configures the development connection from environment variables:
export default {
  development: {
    client: 'postgresql',
    connection: {
      host:     process.env.HOSTDB,
      user:     process.env.USERDB,
      password: process.env.PASSWORDDB,
      database: process.env.DATABASEDB,
      port:     process.env.PORTDB,
    },
    migrations: { directory: './migrations', tableName: 'knex_migrations' },
    seeds:      { directory: './seeds' }
  }
}
Run migrations with npx knex migrate:latest and seed data with npx knex seed:run from the back/ directory.

Winston logging

The Winston logger instance is created in src/config/logger.js and imported by httpLogger. Log levels map directly to HTTP status ranges (see middleware pipeline above). All application log output goes through this single logger, making it straightforward to add transports (file, external service) in one place.

Swagger

Swagger documentation is auto-generated by swagger-autogen and written to src/config/swagger-output.json. The Swagger UI is served at:
http://localhost:3001/api-docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
Regenerating the spec after adding routes requires re-running the swagger-autogen script defined in package.json.

Architecture overview

How the frontend and backend fit together, CORS setup, and the authentication flow.

Frontend architecture

Next.js App Router layout, Zustand state, services layer, and shadcn/ui components.

Build docs developers (and LLMs) love