Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Windows Tipps & SecurityGoogles neues Betriebssystem ist da – heißt jetzt aber komplett anders(22.09.2026 um 12:08 Uhr)
Windows Tipps & SecurityGooglebook-Premiere: Alle 5 Modelle ausprobiert – zwei stechen heraus(22.09.2026 um 12:30 Uhr)
Sichere ProgrammierungDeprecation notice: All-platform CodeQL bundle(22.09.2026 um 11:21 Uhr)
Sichere ProgrammierungCodeSOD: The John Cage Variable(22.09.2026 um 08:30 Uhr)
Windows Tipps & SecurityGoogles neues Betriebssystem ist da – heißt jetzt aber komplett anders(22.09.2026 um 12:08 Uhr)
Windows Tipps & SecurityGooglebook-Premiere: Alle 5 Modelle ausprobiert – zwei stechen heraus(22.09.2026 um 12:30 Uhr)
Sichere ProgrammierungDeprecation notice: All-platform CodeQL bundle(22.09.2026 um 11:21 Uhr)
Sichere ProgrammierungCodeSOD: The John Cage Variable(22.09.2026 um 08:30 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Stop Writing Spaghetti API Routes: A Practical Guide to Clean Express.js Architecture

Every developer has been there. You start a new Node.js project, spin up Express, and everything feels clean. Then three months later, your routes/index.js is 800 lines of spaghetti and you dread opening it. This is the story of how most…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Every developer has been there. You start a new Node.js project, spin up Express, and everything feels clean. Then three months later, your routes/index.js is 800 lines of spaghetti and you dread opening it.



This is the story of how most Express apps die — not from bad intentions, but from the absence of a clear structure from day one.



Today, I want to walk you through a practical architecture that scales. One that won't make your future self cry.






The Problem With "Just Express"



Express is famously un-opinionated. That's its superpower and its curse. It gives you maximum flexibility, which means it gives you maximum rope to hang yourself with.



A typical early-stage Express app looks like this:




// app.js — the everything file
const express = require('express');
const app = express();

app.get('/users', async (req, res) => {
const db = require('./db');
const users = await db.query('SELECT * FROM users');
// business logic mixed in...
const filtered = users.filter(u => u.active);
res.json(filtered);
});

app.post('/users', async (req, res) => {
// validation, DB calls, email sending — all in one place
// 60 more lines...
});

// 20 more routes...






Everything is tangled. Routes do too much. Testing is painful. Onboarding a new dev is a nightmare.






The Architecture That Actually Scales



Here is the folder structure I use for every serious Express project:




src/
config/
index.js # env vars, constants
controllers/
user.controller.js
services/
user.service.js
repositories/
user.repository.js
routes/
index.js
user.routes.js
middlewares/
auth.middleware.js
validate.middleware.js
error.middleware.js
models/
user.model.js
utils/
logger.js
response.js
app.js
server.js






Let me break down what each layer does and why it matters.






Layer 1: Routes — Just Routing, Nothing Else



Your route files should be boring. They declare endpoints and delegate to controllers. That is it.




// routes/user.routes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/user.controller');
const { authenticate } = require('../middlewares/auth.middleware');
const { validate } = require('../middlewares/validate.middleware');
const { createUserSchema } = require('./schemas/user.schema');

router.get('/', authenticate, userController.getAll);
router.get('/:id', authenticate, userController.getById);
router.post('/', validate(createUserSchema), userController.create);
router.put('/:id', authenticate, userController.update);
router.delete('/:id', authenticate, userController.remove);

module.exports = router;






Readable in 10 seconds. You know exactly what each route does, what middleware runs, and where the logic lives.






Layer 2: Controllers — Handle HTTP, Nothing Else



Controllers translate HTTP requests into service calls. They should not contain business logic.




// controllers/user.controller.js
const userService = require('../services/user.service');
const { successResponse, errorResponse } = require('../utils/response');

const getAll = async (req, res, next) => {
try {
const { page = 1, limit = 20 } = req.query;
const users = await userService.getAllUsers({ page, limit });
return successResponse(res, users);
} catch (err) {
next(err);
}
};

const create = async (req, res, next) => {
try {
const user = await userService.createUser(req.body);
return successResponse(res, user, 201);
} catch (err) {
next(err);
}
};

module.exports = { getAll, create };






Notice: no SQL, no business rules, no email sending. Just HTTP in, service call, HTTP out.






Layer 3: Services — Where the Real Logic Lives



This is the heart of your app. Services contain business rules, orchestrate multiple operations, and are completely independent of HTTP.




// services/user.service.js
const userRepository = require('../repositories/user.repository');
const emailService = require('./email.service');
const { AppError } = require('../utils/errors');

const createUser = async (data) => {
const existing = await userRepository.findByEmail(data.email);
if (existing) {
throw new AppError('Email already in use', 409);
}

const user = await userRepository.create(data);
await emailService.sendWelcomeEmail(user.email, user.name);

return user;
};

const getAllUsers = async ({ page, limit }) => {
const offset = (page - 1) * limit;
return userRepository.findAll({ limit, offset });
};

module.exports = { createUser, getAllUsers };






This layer is the easiest to unit test because it has zero HTTP coupling. You can call userService.createUser({...}) directly in tests without spinning up a server.






Layer 4: Repositories — Database Isolation



Repositories abstract your database access. If you ever switch from MySQL to PostgreSQL, or add a caching layer, you only change the repository.




// repositories/user.repository.js
const db = require('../config/database');

const findAll = async ({ limit, offset }) => {
return db('users')
.select('id', 'name', 'email', 'created_at')
.where('active', true)
.limit(limit)
.offset(offset);
};

const findByEmail = async (email) => {
return db('users').where({ email }).first();
};

const create = async (data) => {
const [id] = await db('users').insert(data);
return findById(id);
};

module.exports = { findAll, findByEmail, create };









The Error Middleware That Saves Lives



A centralized error handler is non-negotiable. Every next(err) call in your controllers flows here:




// middlewares/error.middleware.js
const { AppError } = require('../utils/errors');
const logger = require('../utils/logger');

const errorHandler = (err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
});

if (err instanceof AppError) {
return res.status(err.statusCode).json({
success: false,
message: err.message,
});
}

// Unexpected errors — don't leak details
return res.status(500).json({
success: false,
message: 'Internal server error',
});
};

module.exports = errorHandler;






One place. All errors. No scattered res.status(500).json(...) calls across 30 controllers.






Wiring It All Together






// app.js
const express = require('express');
const routes = require('./routes');
const errorHandler = require('./middlewares/error.middleware');
const { requestLogger } = require('./middlewares/logger.middleware');

const app = express();

app.use(express.json());
app.use(requestLogger);

// All routes under /api/v1
app.use('/api/v1', routes);

// Error handler MUST be last
app.use(errorHandler);

module.exports = app;









// routes/index.js
const express = require('express');
const router = express.Router();

router.use('/users', require('./user.routes'));
router.use('/products', require('./product.routes'));
router.use('/auth', require('./auth.routes'));

module.exports = router;









The Payoff



This might feel like more files upfront. And it is. But here is what you gain:



Testing becomes trivial. You can test services without HTTP. You can mock repositories without a real database.



Onboarding is fast. A new developer looks at the folder structure and immediately knows where to add a feature.



Changes are isolated. Swapping your ORM? Only the repository layer changes. Changing email providers? Only the email service.



Debugging is easier. You know exactly which layer an error belongs to. Is it HTTP? Controller. Is it business logic? Service. Is it a query? Repository.






One Rule to Summarize Everything



Each layer should only talk to the layer directly below it. Routes call controllers. Controllers call services. Services call repositories. Never skip layers. Never go sideways.



That single rule, consistently applied, is the difference between a codebase you enjoy working in and one you dread.



Start your next Express project with this structure. Your future self will thank you.






Have a different approach you prefer? I would love to hear it in the comments. Architecture is one of those things where there is rarely one right answer — only trade-offs worth discussing.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Stop Writing Spaghetti API Routes: A Practical Guide to Clean Express.js Architecture

Thematisch verwandte Begriffe: Stop, Writing, Spaghetti, Routes · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94493 | A vulnerability was detected in Gigatech PDV5701 1.0.31_240305_112640. T…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick