We're in 2025, and I'm still seeing codebases treat APIs like they're just "endpoints that return JSON." If your API design hasn't evolved past basic CRUD routes, you're leaving performance, scalability, and developer experience on the table.
Here's what needs to change.
1. Stop Returning Everything by Default
The Problem:
// 2015 mindset
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(user); // Returns 47 fields no one asked for
});
Why It's Bad:
- Kills mobile performance
- Exposes internal data structures
- Forces frontend to do filtering
- Makes caching nearly impossible
Modern Approach:
app.get('/api/users/:id', async (req, res) => {
const fields = req.query.fields?.split(',') || ['id', 'name', 'email'];
const user = await db.users.findById(req.params.id, { select: fields });
res.json(user);
});
Let the client request what it needs. GraphQL taught us this years ago—REST can do it too.
2. Pagination Without Limits Is Criminal
The Problem:
app.get('/api/products', async (req, res) => {
const products = await db.products.find(); // All 50,000 rows
res.json(products);
});
Why It's Bad:
- One request can tank your database
- No way to handle growth
- Timeouts become user experienc e
Modern Approach:
`
app.get('/api/products', async (req, res) => {
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
const cursor = req.query.cursor;
const products = await db.products.find({
where: cursor ? { id: { gt: cursor } } : {},
limit: limit + 1
});
const hasNext = products.length > limit;
const items = hasNext ? products.slice(0, -1) : products;
res.json({
data: items,
cursor: hasNext ? items[items.length - 1].id : null
});
});
`
Cursor-based pagination. Predictable load. Infinite scroll that doesn't destroy your server.
3. Error Responses Are Not an Afterthought
The Problem:
app.post('/api/orders', async (req, res) => {
try {
const order = await createOrder(req.body);
res.json(order);
} catch (err) {
res.status(500).json({ error: 'Something went wrong' });
}
});
Frontend devs hate you for this.
Modern Approach:
`app.post('/api/orders', async (req, res) => {
try {
const order = await createOrder(req.body);
res.json({ data: order });
} catch (err) {
if (err.name === 'ValidationError') {
return res.status(400).json({
error: {
code: 'VALIDATION_FAILED',
message: 'Invalid order data',
fields: err.details
}
});
}
if (err.name === 'InsufficientStock') {
return res.status(409).json({
error: {
code: 'INSUFFICIENT_STOCK',
message: 'Product out of stock',
productId: err.productId
}
});
}
// Log actual error server-side
logger.error(err);
res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'Failed to create order'
}
});
}
});`
Structured errors. Actionable feedback. Frontend can actually handle this.
4. Rate Limiting Isn't Optional Anymore
The Problem:
No rate limiting = open invitation for abuse, accidents, or runaway scripts that cost you money.
Modern Approach:
`import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests',
retryAfter: req.rateLimit.resetTime
}
});
}
});
app.use('/api/', limiter);`
Protect your infrastructure. It's 2025—this should be default.
5. Versioning From Day One
The Problem:
app.get('/api/users', ...); // What happens when this needs to change?
Modern Approach:
app.get('/api/v1/users', ...);
When you need breaking changes:
app.get('/api/v2/users', ...); // New behavior
app.get('/api/v1/users', ...); // Still works for old clients
Versioning isn't premature optimization. It's respecting your users and your future self.
6. Stop Ignoring HTTP Status Codes
The Problem:
res.status(200).json({ error: 'User not found' }); // WHY
Modern Approach:
200 — Success
201 — Created
400 — Bad request (client's fault)
401 — Unauthorized
403 — Forbidden
404 — Not found
409 — Conflict (duplicate, constraint violation)
422 — Unprocessable entity (validation failed)
429 — Rate limited
500 — Server error (your fault)
Use them correctly. Clients, monitoring tools, and caching layers depend on this.
7. Caching Headers Are Free Performance
The Problem:
Every request hits your database, even for data that hasn't changed in weeks.
Modern Approach:
`app.get('/api/products/:id', async (req, res) => {
const product = await db.products.findById(req.params.id);
res.set({
'Cache-Control': 'public, max-age=300', // 5 minutes
'ETag': generateETag(product)
});
res.json(product);
});`
CDNs, browsers, and proxies will do the work for you. Let them.
The Real Cost of Legacy API Patterns
I've inherited codebases where:
A
- single unoptimized endpoint cost $800/month in database reads
- Missing rate limits led to accidental DDoS from a bug in a mobile app
- Poor error handling meant every bug report was "something went wrong" Modern API design isn't about being trendy. It's about building systems that scale, survive user mistakes, and don't require a full rewrite in two years. What outdated API patterns are you still seeing in 2025? Drop them in the comments.
Top comments (0)