ποΈ 1. Scalable Folder Structure β The Foundation
Your folder structure is the blueprint of scalability. A bloated index.js
or random file placements are signs of short-term thinking.
β Recommended Folder Layout:
src/
βββ config/ # Environment configs, DB setup
βββ controllers/ # Route handlers
βββ routes/ # API routes
βββ models/ # Data schemas or ORM models
βββ middlewares/ # Custom middlewares
βββ services/ # Business logic layer
βββ utils/ # Utility functions/helpers
βββ validations/ # Input validators
βββ index.js # Entry point
β Why this works:
- Separation of concerns
- Easier unit testing
- Scales better with team size and complexity
π‘ Pro Tip: Each route/controller pair should be feature-specific. For example,
authController.js
,authRoutes.js
.
βοΈ 2. Middleware Architecture β Your Express Engine
Express is middleware-driven. A clear pattern makes your API predictable.
π οΈ Example: Modular Logger
// middlewares/logger.js
module.exports = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
};
// index.js
const logger = require('./middlewares/logger');
app.use(logger);
β Common Mistake:
app.use((req, res, next) => {
// anonymous logger
});
Anonymous middlewares clutter the stack trace and complicate testing.
π 3. Security, Rate Limiting, and Config Management
π Security Essentials
- Helmet β Sets secure HTTP headers
- CORS β Restrict domain access
- Sanitize Inputs β Prevent NoSQL/SQL injection
const helmet = require('helmet');
const cors = require('cors');
const mongoSanitize = require('express-mongo-sanitize');
app.use(helmet());
app.use(cors());
app.use(mongoSanitize());
π§ Rate Limiting with express-rate-limit
Prevent brute-force attacks or misuse of public endpoints.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 mins
max: 100,
message: 'Too many requests from this IP, please try again later.',
});
app.use(limiter);
π¦ Environment Configurations
Avoid hardcoding credentials or config values.
// .env
PORT=5000
MONGO_URI=mongodb://localhost:27017/myapp
require('dotenv').config();
mongoose.connect(process.env.MONGO_URI);
π§ 4. Common Pitfalls (and How to Avoid Them)
π΅ Callback Hell
fs.readFile('a.txt', (err, data) => {
fs.readFile('b.txt', (err2, data2) => {
// hell continues...
});
});
β
Use Promises or async/await
:
const util = require('util');
const readFile = util.promisify(fs.readFile);
const data = await readFile('a.txt');
π₯ Unhandled Promise Rejections
router.get('/user/:id', async (req, res) => {
const user = await User.findById(req.params.id); // π΄ if error, app crashes
res.json(user);
});
β
Use catchAsync()
wrapper:
// utils/catchAsync.js
module.exports = (fn) => (req, res, next) => {
fn(req, res, next).catch(next);
};
router.get('/user/:id', catchAsync(async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
}));
π¬ 5. Versioning Your API
Versioning helps when you want to roll out breaking changes without affecting existing clients.
app.use('/api/v1/users', userRoutes);
π§ Best Practice: Keep different versions in separate folders if needed (
v1/
,v2/
)
π§ͺ 6. Input Validation & Sanitization
Never trust client inputβvalidate every field.
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
router.post('/register', async (req, res, next) => {
const { error } = userSchema.validate(req.body);
if (error) return res.status(400).json({ error: error.details[0].message });
});
π§° 7. Logging with Morgan + Winston
π Request Logs
const morgan = require('morgan');
app.use(morgan('dev')); // logs method, URL, status, response time
ποΈ Persistent App Logs
// logger.js
const winston = require('winston');
module.exports = winston.createLogger({
level: 'info',
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' })
]
});
β 8. Final Checklist for Scalability
Area | Best Practice |
---|---|
Error Handling | Central error middleware using app.use(errHandler)
|
Validation | Use Joi , Yup , or Zod
|
Testing | Use Jest and Supertest for route testing |
Docs | Auto-generate with Swagger or Postman collection |
Code Style | Enforce with ESLint + Prettier |
Monitoring | Use tools like New Relic, Sentry, or PM2 |
π§ Summary β Lessons from Real Projects
β
Modularize your codebase using folders
β
Apply security best practices from Day 1
β
Centralize error handling and input validation
β
Use async/await with proper error propagation
β
Monitor and log everythingβdonβt fly blind
π Want a starter boilerplate that includes folder structure, rate limiting, validation, error handling, and logging? Drop a comment or DM me β Iβll share a GitHub template I personally use in projects.
Top comments (2)
Really appreciate how you break down the folder structure and error handling, it saves so much pain later on. Do you have a favorite validation library or combo for large APIs?
This is extremely impressive, I honestly wish Iβd had all this written down when I was starting out