Skip to Content
chalvien 1.0 is released
DocumentationGuidesSecurityAuthenticationJWTJWT Folder Structure

Authentication Folder Structure

Express + Prisma + PostgreSQL Backend

This document proposes a clean, production-ready folder structure for implementing authentication in an Express API using Prisma ORM.

The structure separates responsibilities clearly and keeps the codebase maintainable as the project grows.

Stack:

  • Express API
  • Prisma ORM
  • PostgreSQL
  • JWT authentication
  • bcrypt password hashing
  • Next.js frontend consuming the API

Design Principles

The structure follows these principles:

PrincipleDescription
Separation of concernsControllers, middleware, services, and utilities are separated
Feature groupingAuthentication logic lives under auth
Thin controllersControllers delegate logic to services
TestabilityBusiness logic is isolated in services
ScalabilityEasy to extend with additional modules
src/ app.js server.js config/ env.js cookies.js prisma/ client.js routes/ index.js auth.routes.js user.routes.js controllers/ auth/ login.controller.js logout.controller.js refresh.controller.js bootstrap.controller.js me.controller.js services/ auth/ auth.service.js token.service.js password.service.js middleware/ authenticate.js requireRole.js errorHandler.js utils/ jwt.js logger.js validators/ auth.validators.js types/ request.types.js

Folder Responsibilities

app.js

Creates and configures the Express application.

const express = require("express"); const cookieParser = require("cookie-parser"); const routes = require("./routes"); const app = express(); app.use(express.json()); app.use(cookieParser()); app.use("/api", routes); module.exports = app;

server.js

Starts the HTTP server.

server.js
const app = require("./app"); const PORT = process.env.PORT || 3001; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });

config/

Configuration utilities.

Example:

config/ env.js cookies.js

cookies.js

cookies.js
module.exports = { refreshCookie: { httpOnly: true, sameSite: "strict", secure: process.env.NODE_ENV === "production", path: "/api/auth" } };

prisma/

Prisma client initialization.

prisma/ client.js

Example:

const { PrismaClient } = require("@prisma/client"); const prisma = new PrismaClient(); module.exports = prisma;

routes/

Defines API routes and connects them to controllers.

routes/ index.js auth.routes.js

Example:

auth.routes.js
const express = require("express"); const login = require("../controllers/auth/login.controller"); const refresh = require("../controllers/auth/refresh.controller"); const logout = require("../controllers/auth/logout.controller"); const bootstrap = require("../controllers/auth/bootstrap.controller"); const router = express.Router(); router.post("/login", login); router.post("/refresh", refresh); router.post("/logout", logout); router.post("/bootstrap", bootstrap); module.exports = router;

controllers/

Controllers handle HTTP request/response.

They should remain thin and delegate logic to services.

controllers/ auth/ login.controller.js logout.controller.js refresh.controller.js bootstrap.controller.js

Example controller:

const authService = require("../../services/auth/auth.service"); async function login(req, res) { const result = await authService.login(req.body); res.json(result); } module.exports = login;

services/

Services contain business logic.

services/ auth/ auth.service.js token.service.js password.service.js

Example:

token.service.js

token.service.js
const jwt = require("jsonwebtoken"); function generateAccessToken(payload) { return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: process.env.JWT_ACCESS_TTL }); } module.exports = { generateAccessToken };

middleware/

Express middleware functions.

middleware/ authenticate.js requireRole.js errorHandler.js

Example:

function authenticate(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ error: "Missing token" }); } next(); }

utils/

General-purpose utilities.

utils/ jwt.js logger.js

Example:

const jwt = require("jsonwebtoken"); function verifyToken(token) { return jwt.verify(token, process.env.JWT_SECRET); } module.exports = { verifyToken };

validators/

Input validation logic.

validators/ auth.validators.js

Example:

function validateLogin(body) { if (!body.email || !body.password) { throw new Error("Email and password required"); } }

Request Flow

Typical request lifecycle: Client Request | v Route | v Controller | v Service | v Prisma ORM | v PostgreSQL | v Response

Example Login Flow

POST /api/auth/login | v auth.routes.js | v login.controller.js | v auth.service.js | v password.service.js token.service.js | v Prisma user lookup | v Return JWT + cookie

Why This Structure Works Well

Benefits:

BenefitExplanation
MaintainabilityCode responsibilities are clear
TestabilityServices can be unit tested
ScalabilityEasy to add new modules
SecurityAuth logic centralized
Clean architectureControllers remain simple

Typical Growth Path

As the project evolves, you may add:

modules/ users/ orders/ vessels/ inspections/

Each module can follow the same pattern:

routes controllers services validators

Summary

This folder structure supports:

  • clear separation of concerns
  • scalable Express API design
  • clean authentication implementation
  • easy integration with Prisma ORM
  • multiple frontend clients (Next.js, mobile, integrations)

The backend remains the central authority for authentication and authorization.