Stop Writing APIs Like It's 2015
Source: Dev.to
Introduction
We’re in 2025, and many codebases still treat APIs as simple “endpoints that return JSON.” If your API design hasn’t evolved past basic CRUD routes, you’re sacrificing performance, scalability, and developer experience.
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 the frontend to filter
- 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 only 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 degrade user experience
Modern Approach (Cursor‑based Pagination)
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
});
});
Predictable load and 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 developers hate this vague response.
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 give actionable feedback, allowing the frontend to handle failures gracefully.
4. Rate Limiting Isn’t Optional Anymore
The Problem
No rate limiting leaves your API open to abuse, accidental runaway scripts, and costly spikes.
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—this should be default in 2025.
5. Versioning From Day One
The Problem
app.get('/api/users', …); – What happens when the contract changes?
Modern Approach
app.get('/api/v1/users', …); // Existing clients
app.get('/api/v2/users', …); // New behavior
Versioning isn’t premature optimization; it respects your users and your future self.
6. Stop Ignoring HTTP Status Codes
The Problem
res.status(200).json({ error: 'User not found' }); // WRONG
Modern Approach – Use the appropriate codes:
- 200 – Success
- 201 – Created
- 400 – Bad request (client error)
- 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)
Correct status codes enable clients, monitoring tools, and caching layers to work properly.
7. Caching Headers Are Free Performance
The Problem
Every request hits the 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 handle the heavy lifting for you.
The Real Cost of Legacy API Patterns
- A single unoptimized endpoint can cost $800/month in database reads.
- Missing rate limits once caused an accidental DDoS from a buggy mobile app.
- Poor error handling turned every bug report into “something went wrong.”
Modern API design isn’t about being trendy; it’s about building systems that scale, survive user mistakes, and don’t need a complete rewrite in two years.
What outdated API patterns are you still seeing in 2025? Drop them in the comments.