Your API Works. But Does It Slap?
So you've been vibing. You prompted your way into a backend, Cursor autocompleted half the routes, and somehow the thing actually works. Respect. Genuinely.
Most developers don't actually design REST APIs, they just return JSON and call it done. But professional API design is about more than making things work. It's about consistency, scalability, predictability, and developer experience.
This guide walks through the 10 most common REST API mistakes and how to fix each one with real code examples.
1. Use Resource-Based URLs (No Verbs)
Be honest. You have endpoints that look like this:
GET /getUsers
POST /createUser
PUT /updateUser/4
DELETE /deleteUser/5
It feels natural! It reads like English! It is also wrong.
REST is about resources, not actions. Your HTTP method already defines the action, so don't repeat it in the URL.
❌ Wrong
GET /getUsers
POST /createUser
DELETE /deleteUser/5
✅ Correct
GET /users
POST /users
DELETE /users/5
The HTTP method tells you what to do. The URL tells you what resource you're acting on.
| Method | Meaning |
|---|---|
| GET | Retrieve |
| POST | Create |
| PUT / PATCH | Update |
| DELETE | Remove |
Rule: URLs are nouns. HTTP methods are verbs.
2. Use The Right HTTP Status Codes Correctly
This one hurts to see. An API that returns 200 OK even when things go wrong:
Returning 200 OK for everything, including errors, is a common anti-pattern. Status codes are part of your API contract.
❌ Wrong
HTTP 200 OK
{
"status": "error",
"message": "User not found"
}
✅ Correct
// Resource found
HTTP 200 OK
// Resource created
HTTP 201 created
// Bad Request
HTTP 400 Bad request
// Validation error
HTTP 422 Unprocessable Entity
// Not authenticated
HTTP 401 Unauthorized
// Authenticated but not allowed
HTTP 403 Forbidden
// Resource doesn't exist
HTTP 404 Not Found
// Server error
HTTP 500 Internal Server Error
Example in Node.js (Express)
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: { code: 404, message: 'User not found' }
});
}
return res.status(200).json({ data: user });
});
3. Keep JSON Naming Consistent
Mixing camelCase, snake_case, and lowercase across endpoints forces front-end developers to guess the format and that slows everyone down.
❌ Wrong
// Endpoint A
{ "userName": "alice", "user_email": "alice@example.com" }
// Endpoint B
{ "username": "bob", "userEmail": "bob@example.com" }
✅ Correct — pick one and stick to it
snake_case (common in Laravel/Python APIs):
{
"user_name": "alice",
"user_email": "alice@example.com",
"created_at": "2024-01-15T10:00:00Z"
}
camelCase (common in Node.js APIs):
{
"userName": "alice",
"userEmail": "alice@example.com",
"createdAt": "2024-01-15T10:00:00Z"
}
Rule: It doesn't matter which you choose. What matters is that you use it everywhere.
4. Version Your API from Day One
Skipping versioning feels harmless until you need to change a response structure and suddenly every client breaks.
❌ Wrong
/users ← you change this and everything breaks
✅ Correct
/api/v1/users ← stable
/api/v2/users ← new version when needed
Future you is begging present you to do this one thing.
Example: Express Router with versioning
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
Old clients keep using v1. New clients adopt v2. No broken production systems.
5. Implement Pagination
Returning 10,000 records in a single response will slow your server, waste bandwidth, and hurt user experience.
❌ Wrong
GET /users ← returns all 8,700 users
← your server starts crying
← your users leave
✅ Correct
GET /users?page=1&limit=10
Expected response structure
{
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
],
"meta": {
"current_page": 1,
"per_page": 10,
"total": 1000,
"last_page": 100
}
}
Example in Node.js
app.get('/users', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
const { rows: users, count: total } = await User.findAndCountAll({
limit,
offset
});
res.status(200).json({
data: users,
meta: {
current_page: page,
per_page: limit,
total,
last_page: Math.ceil(total / limit)
}
});
});
6. Separate Authentication from Authorization
A lot of vibe code treats these like they're the same. They're not.
These are two different things and mixing them creates security gaps.
| Concept | Question it answers |
|---|---|
| Authentication | Who are you? (identity) |
| Authorization | What are you allowed to do? (permissions) |
✅ Correct approach in Node.js (JWT)
// Middleware: Authentication
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
res.status(401).json({ error: 'Invalid token' });
}
};
// Middleware: Authorization
const authorize = (role) => (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
// Usage
app.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
Never rely on front-end validation alone. Hidden buttons don't protect endpoints. Security must be enforced on the server.
7. Return Structured Error Responses
Vague error messages like "Something went wrong" are useless for front-end developers, for logging, and for debugging.
❌ Wrong
HTTP 500
{ "message": "Something went wrong" }
This tells nobody anything. What went wrong? Where? Why? Your front-end team can't handle it properly. You can't debug it. It's just vibes in JSON form.
✅ Correct
HTTP 404
{
"error": {
"code": 404,
"message": "User not found",
"field": null
}
}
HTTP 422
{
"error": {
"code": 422,
"message": "Validation failed",
"fields": {
"email": "The email field is required.",
"password": "Password must be at least 8 characters."
}
}
}
Reusable error helper in Node.js
const errorResponse = (res, statusCode, message, fields = null) => {
return res.status(statusCode).json({
error: { code: statusCode, message, ...(fields && { fields }) }
});
};
// Usage
errorResponse(res, 404, 'User not found');
errorResponse(res, 422, 'Validation failed', { email: 'Email is required' });
8. Use Query Parameters for Filtering and Sorting
Creating a new endpoint for every filter combination doesn't scale.
❌ Wrong
GET /getActiveUsersSortedByName
GET /getInactiveUsersSortedByDate
GET /getUsersByRole
✅ Correct
GET /users?status=active&sort=name&order=asc
GET /users?status=inactive&sort=created_at&order=desc
GET /users?role=admin
Example in Node.js
app.get('/users', async (req, res) => {
const { status, sort = 'created_at', order = 'asc', role } = req.query;
const where = {};
if (status) where.status = status;
if (role) where.role = role;
const users = await User.findAll({
where,
order: [[sort, order.toUpperCase()]]
});
res.status(200).json({ data: users });
});
9. Enforce Security from Day One
Security is not something you add after deployment. It's built in from the start.
Common mistakes to avoid
| Mistake | Risk |
|---|---|
| No rate limiting | Brute force & abuse |
| No input validation | SQL injection, XSS |
| No HTTPS | Data interception |
| Exposing sensitive fields | Token/password leakage |
Rate limiting in Express
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: { error: { code: 429, message: 'Too many requests' } }
});
app.use('/api/', limiter);
Input validation (using Joi)
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
});
app.post('/users', (req, res) => {
const { error } = userSchema.validate(req.body);
if (error) {
return res.status(422).json({
error: { code: 422, message: error.details[0].message }
});
}
// proceed...
});
Hide sensitive fields (Laravel Resource)
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
// password, token, internal_id are intentionally omitted
];
}
}
10. Design Around Client Needs, Not Your Database
Your database schema is an internal implementation detail. Your API is a public contract.
❌ Wrong mindset
"This is how my database table looks, so this is how my response should look."
✅ Right mindset
"This is what the client needs. I'll shape the response to serve that."
Example: Combining fields and transforming data
Database has: first_name, last_name, dob (date of birth)
Client needs: full_name, age
app.get('/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) return res.status(404).json({ error: { code: 404, message: 'User not found' } });
const age = new Date().getFullYear() - new Date(user.dob).getFullYear();
res.status(200).json({
data: {
id: user.id,
full_name: `${user.first_name} ${user.last_name}`,
email: user.email,
age
// internal columns like dob, password_hash are hidden
}
});
});
Your database can change. Your implementation can evolve. But your API contract must remain stable designed for the client, not for the table.
Summary: What Professional REST APIs Look Like
| Principle | What it means |
|---|---|
| ✅ Resource-based URLs | Nouns in URLs, verbs via HTTP methods |
| ✅ Correct HTTP status codes | 201 for created, 404 for missing, 422 for invalid, etc. |
| ✅ Consistent naming | snake_case or camelCase — never both |
| ✅ API versioning |
/api/v1/ from day one |
| ✅ Pagination | Never return unbounded record sets |
| ✅ Auth + Authz separated | Identity vs. permissions are distinct |
| ✅ Structured errors | Predictable, machine-readable error objects |
| ✅ Query param filtering | One endpoint, flexible behavior |
| ✅ Security by default | HTTPS, rate limiting, validation, no sensitive leaks |
| ✅ Client-first design | API shape driven by what clients need |
The difference between it works and it's well-designed is consistency, predictability, and care for the developer experience. Design it right from the beginning, your future self (and your teammates) will thank you.
You don't have to do all of this perfectly on day one. But the earlier you build these habits in, the less you'll be fighting your own code later.
The AI can write the routes. You have to make them good.
Ship clean. 🚀
Top comments (0)