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:
| Principle | Description |
|---|---|
| Separation of concerns | Controllers, middleware, services, and utilities are separated |
| Feature grouping | Authentication logic lives under auth |
| Thin controllers | Controllers delegate logic to services |
| Testability | Business logic is isolated in services |
| Scalability | Easy to extend with additional modules |
Recommended Project Structure
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.
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.jscookies.js
module.exports = {
refreshCookie: {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
path: "/api/auth"
}
};
prisma/
Prisma client initialization.
prisma/
client.jsExample:
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.jsExample:
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.jsExample 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.jsExample:
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.jsExample:
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.jsExample:
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.jsExample:
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 + cookieWhy This Structure Works Well
Benefits:
| Benefit | Explanation |
|---|---|
| Maintainability | Code responsibilities are clear |
| Testability | Services can be unit tested |
| Scalability | Easy to add new modules |
| Security | Auth logic centralized |
| Clean architecture | Controllers 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
validatorsSummary
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.