Why Your Private API Doesn't Need HTTP Status Codes (And That's Fine)
Always returning HTTP 200 sounds wrong, but it might simplify private APIs. Here's why status codes create unnecessary complexity for internal systems.

The Comment That Started a War
Mehul Mohan posted a video explaining how his startup's API always returns HTTP 200 status code. Not sometimes. Not usually. Always.
Even when there's a validation error. Even when the user ID doesn't match. Even when something goes wrong.
The comments exploded.
"This is a nightmare!"
"There's a reason HTTP methods and status codes exist!"
"Returning 200 when there's an error is wild to me!"
Developers were furious. REST API purists came out swinging. The video became a battleground between "follow the standards" and "question everything."
But here's the thing: Mehul wasn't wrong. He was just solving a different problem than what most developers face.
And understanding why this approach works reveals something important about how we build systems.
The Two Types of APIs (And Why It Matters)
Before we argue about status codes, we need to understand a fundamental distinction most developers miss:
There are two completely different types of APIs.
1. Public APIs
These are APIs you expose to external developers. Stripe's payment API. Twitter's developer API. GitHub's REST API.
Characteristics:
- Unknown consumers
- Need clear documentation
- Must follow standards (REST, GraphQL, etc.)
- Breaking changes hurt people
- Semantics matter (GET, POST, PUT, DELETE all have meaning)
For public APIs, yes, follow every REST convention you learned. Use proper status codes. Use semantic HTTP methods. Make it predictable.
Nobody's arguing against this.
2. Private APIs
These are APIs your frontend talks to. Internal microservices. Backend-to-backend communication within your company.
Characteristics:
- You control both sides (client and server)
- Can change anytime without external impact
- Documentation is internal
- Speed of development > rigid standards
- Customization is possible
This is where things get interesting.
When you're building Twitter's web app, nobody outside your company sees how your React app talks to your backend. Users don't care. Other developers can't access it. It's completely internal.
So why are you following rules designed for public APIs?
The GraphQL Precedent
Remember when GraphQL got popular?
People said: "Wait, you're putting EVERYTHING in POST requests? You're sending the entire query structure in the request body? That's not REST!"
And they were right. It wasn't REST.
But GraphQL worked. Facebook used it at massive scale. Companies adopted it. It solved real problems.
Why did it work?
Because GraphQL questioned the assumption that HTTP methods needed to carry semantic meaning. It said: "What if we just use POST for everything and put all the information in the request body?"
And the sky didn't fall.
Mehul's approach is similar. He's just taking it one step further.
The Status Code Problem Nobody Talks About
Let's say you're building an API endpoint: updateUserProfile
This endpoint:
- Updates the profile URL
- Deletes the old image from object storage
- Validates user permissions
Question: What HTTP method do you use?
Option 1: PATCH (you're updating data) Option 2: DELETE (you're deleting the old image) Option 3: POST (you're creating a new state)
All three make sense. All three are "correct" by different interpretations.
Now imagine you're creating 10,000 API endpoints for your startup. How much time do you waste debating: "Should this be PATCH or PUT?" "Is this a DELETE or a POST?"
Hours. Across your entire team. Every week.
Mehul's Solution: Just Use POST
For private APIs, every mutation is a POST request.
Why?
- No debates about semantic correctness
- Consistency across codebase
- Faster development
- Simpler mental model
GET for reads. POST for writes. Done.
Is this REST? No.
Does it matter for a private API? Also no.
The Status Code Chaos
Now let's talk about status codes.
You build updateUserProfile again. The user sends a request with a mismatched user ID.
What status code do you return?
400 Bad Request? (User sent invalid data)
401 Unauthorized? (User isn't authorized to update this profile)
404 Not Found? (User ID doesn't match any existing user)
All three are defensible. All three follow HTTP specifications.
But your backend knows exactly what went wrong: "User ID mismatch."
So why encode that information in a status code when you can just... say it?
The Traditional Approach:
json
HTTP/1.1 401 Unauthorized
{
"error": "User ID does not match authenticated user"
}
You've now communicated the error in TWO places:
- Status code (401)
- Body ("User ID does not match...")
The body is the source of truth. The status code is redundant.
Mehul's Approach:
json
HTTP/1.1 200 OK
{
"success": false,
"error": "USER_ID_MISMATCH",
"message": "User ID does not match authenticated user"
}
Everything you need is in the body. Status code is just: "Request reached the server successfully."
But What About Monitoring?
The most common pushback: "If everything returns 200, how do you monitor your system?"
Mehul's answer: Health check endpoints.
Instead of relying on status codes scattered across thousands of endpoints, you have dedicated health check APIs:
javascript:
GET /health/backend
Response:
{
"status": "healthy",
"database": {
"status": "connected",
"latency": "12ms"
},
"cpu": {
"usage": "34%",
"status": "healthy"
},
"memory": {
"usage": "2.1GB / 8GB",
"status": "healthy"
}
}
Your monitoring tool calls this every 10 seconds.
If the response is anything other than HTTP 200, something catastrophic happened:
- Backend is down
- Reverse proxy is down
- SSL certificate expired
- Network failure
This is actually MORE reliable than scattered status codes.
Why?
Because if you get a 500 error from a regular endpoint, where did it come from?
- Your backend?
- Nginx reverse proxy?
- Cloudflare?
- Some middleware layer?
You don't know without digging into headers and logs.
But if your health check returns non-200, you know immediately: The backend is unreachable.
The State Machine Problem
Here's something they teach you in computer science: State machines.
Every system is a state machine. Your API request goes through states:
- Client sends request
- Request reaches reverse proxy
- Proxy forwards to backend
- Backend processes request
- Backend queries database
- Backend responds
- Proxy returns response
- Client receives response
At EACH step, something can fail.
Traditional approach with status codes creates complexity:
- What if proxy returns 500?
- What if backend returns 500?
- What if database times out (is that 500 or 504?)
- What if request is malformed (400? 422?)
You now have dozens of possible states to handle.
Mehul's approach simplifies this:
Status code states:
- 200: Backend responded successfully
- Non-200: Something catastrophic happened before backend could respond
Error information:
- Always in response body
- Always structured the same way
- Always easy to parse
Fewer states = simpler system = fewer bugs.
The Real-World Test: Does It Scale?
Mehul's startup handles 1,000-2,000 requests per second.
That's not a toy project. That's production scale.
The system works. Has been working. Will keep working.
Why?
Because at scale, you realize:
- Status codes don't prevent bugs
- REST conventions don't improve uptime
- Semantic HTTP methods don't make debugging easier
What matters:
- Clear error messages (in body)
- Fast incident response (health checks)
- Consistent patterns (always POST, always 200)
- Good logging and monitoring
When This Approach Makes Sense
Use Mehul's "always 200" approach when:
✅ Building private APIs (internal only) ✅ You control both client and server ✅ Team wants faster development velocity ✅ You have robust health check infrastructure ✅ Error messages in body are descriptive
Stick with traditional REST when:
❌ Building public APIs ❌ External developers consume your API ❌ You're integrating with systems that expect standard codes ❌ Team is unfamiliar with custom approaches
The Credentials Matter
Before dismissing this as "some random YouTuber's bad take," understand who Mehul Mohan is:
Background:
- Started programming at 13
- Found and reported XSS vulnerability in Google (rewarded $3,133.7)
- Created viral tutorials on Angular, React, Node.js (millions of views)
- Wrote two books (JavaScript, React) at 18-19
- Apple WWDC Scholar (built VR solar system on iPad pre-AI era)
- Founded and runs startup at production scale (1K-2K req/sec)
This isn't theory from someone who's never shipped production code.
This is battle-tested experience from someone who's crashed production backends and learned what actually matters.
Our Take: Context Is Everything
Here's what we think at Art of Code after analyzing this:
Mehul is right... for his use case.
If you're building a startup's private API and you want to move fast, this approach removes unnecessary friction.
But it's not a universal rule.
Public APIs should follow REST conventions. Systems integrating with third parties should use standard status codes.
The real lesson: Question conventions when they slow you down, but understand when to follow them.
Ask yourself:
- Who consumes this API?
- What problem am I actually solving?
- Does this convention help or hurt velocity?
- Am I following rules because they're best practice, or because I never questioned them?
For most startups building internal APIs, Mehul's approach makes sense.
For most companies building public APIs, traditional REST makes sense.
Both are valid. Context determines which to use.
The Code: How It Actually Works
Traditional REST Approach:
javascript:
// Traditional REST API
app.patch('/api/users/:id/profile', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found'
});
}
if (user.id !== req.user.id) {
return res.status(401).json({
error: 'Unauthorized'
});
}
if (!req.body.profileUrl) {
return res.status(400).json({
error: 'Missing profileUrl'
});
}
// Delete old image
await deleteFromStorage(user.oldProfileUrl);
// Update user
user.profileUrl = req.body.profileUrl;
await user.save();
res.status(200).json({ user });
} catch (error) {
res.status(500).json({
error: 'Internal server error'
});
}
});
Problems:
- Multiple status code branches
- Debates about which code to use
- Error info duplicated (code + message)
Mehul's Approach:
javascript:
// Always 200 approach
app.post('/api/updateUserProfile', async (req, res) => {
try {
const user = await User.findById(req.body.userId);
if (!user) {
return res.status(200).json({
success: false,
error: 'USER_NOT_FOUND',
message: 'User not found'
});
}
if (user.id !== req.user.id) {
return res.status(200).json({
success: false,
error: 'USER_ID_MISMATCH',
message: 'User ID does not match authenticated user'
});
}
if (!req.body.profileUrl) {
return res.status(200).json({
success: false,
error: 'MISSING_PROFILE_URL',
message: 'Profile URL is required'
});
}
// Delete old image
await deleteFromStorage(user.oldProfileUrl);
// Update user
user.profileUrl = req.body.profileUrl;
await user.save();
res.status(200).json({
success: true,
data: { user }
});
} catch (error) {
// Only non-200 for catastrophic failures
res.status(500).json({
error: 'CATASTROPHIC_ERROR',
message: error.message
});
}
});
Benefits:
- Single status code (always 200 except catastrophic)
- Clear error codes (
USER_NOT_FOUND,USER_ID_MISMATCH) - Consistent response structure
- No debates about semantics
Health Check Implementation:
javascript:
app.get('/health', async (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date(),
checks: {}
};
// Database check
try {
const start = Date.now();
await db.ping();
health.checks.database = {
status: 'connected',
latency: `${Date.now() - start}ms`
};
} catch (error) {
health.status = 'unhealthy';
health.checks.database = {
status: 'disconnected',
error: error.message
};
}
// CPU check
const cpuUsage = os.loadavg()[0] / os.cpus().length;
health.checks.cpu = {
usage: `${(cpuUsage * 100).toFixed(2)}%`,
status: cpuUsage < 0.8 ? 'healthy' : 'overloaded'
};
// Memory check
const memUsage = process.memoryUsage();
health.checks.memory = {
heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)}MB`,
heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)}MB`,
status: memUsage.heapUsed / memUsage.heapTotal < 0.9 ? 'healthy' : 'high'
};
const statusCode = health.status === 'healthy' ? 200 : 500;
res.status(statusCode).json(health);
});
Monitoring tool calls this every 10 seconds.
If response ≠ 200: Something is catastrophically wrong.
If response = 200: Parse body to check individual component health.
Key Takeaways
HTTP status codes and methods were designed in the 1990s for a different internet with different needs.
For private APIs, simplification often beats standardization - fewer states, faster development, clearer errors.
Health check endpoints are more reliable than scattered status codes for monitoring production systems.
GraphQL proved you don't need semantic HTTP methods - everything can be POST if that's simpler.
Context determines correctness - public APIs need standards, private APIs need velocity.
Question conventions that slow you down - but understand when to follow them.
Always returning 200 isn't "wrong" - it's a different trade-off optimized for internal systems.
The Bottom Line
Mehul's approach isn't universal. It's not "the one true way."
But it's not wrong either.
It's a deliberate choice trading REST purity for development speed, made by someone running production systems at scale.
For your startup's private API? Consider it.
For your company's public API? Probably stick with REST.
For your next side project? Try it and see.
The best API design is the one that:
- Solves your specific problem
- Works at your scale
- Matches your team's velocity needs
- Can be maintained long-term
Sometimes that's REST. Sometimes it's GraphQL. Sometimes it's "always POST, always 200."
All are valid. Choose based on context, not dogma.
What's your take? Are you team "follow REST standards" or team "question everything for private APIs"?
Have you tried this approach? Share your experience in the comments.
Based on the video "Your API Does Not Need Status Codes And Methods" by Mehul Mohan (461K subscribers, 1.2K views)
Video Link : -- Click Me