Your API Works. But Is It Actually Reliable?
Building a REST API is easy. Building one that handles things going wrong? That's where it gets interesting. When I first started working with APIs, I mostly cared about one thing: Does the endpoint return the correct d
Building a REST API is easy. Building one that handles things going wrong? That's where it gets interesting.
When I first started working with APIs, I mostly cared about one thing: Does the endpoint return the correct data?
If I sent a GET request and received JSON, I considered it working.
But there's a lot more to a reliable API than returning 200 OK.
What happens when the client sends invalid data? What if a resource doesn't exist? What if something breaks on the server?
Let's look at a few simple things that can make a Node.js API much more reliable.
1. Use the Right HTTP Status Codes
HTTP status codes aren't just random numbers. They tell the client what happened.
Here are some common ones:
| Status | Meaning |
|---|---|
200 OK |
The request succeeded |
201 Created |
A resource was created |
400 Bad Request |
The client sent an invalid request |
401 Unauthorized |
Authentication is required or invalid |
403 Forbidden |
The client isn't allowed to access the resource |
404 Not Found |
The requested resource doesn't exist |
500 Internal Server Error |
Something unexpected failed on the server |
503 Service Unavailable |
The server temporarily can't handle the request |
For example, if someone requests a user that doesn't exist, returning 500 isn't helpful.
Instead:
app.get("/api/users/:id", (req, res) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return res.status(404).json({
error: "User not found"
});
}
res.json(user);
});
Now the client knows exactly what happened.
2. Never Trust User Input
Just because your API expects valid data doesn't mean it will always receive it.
Imagine an endpoint that creates a new user:
app.post("/api/users", (req, res) => {
const { username } = req.body;
users.push({ username });
res.status(201).json({ message: "User created" });
});
What if someone sends an empty request?
Your API might create a user without a username.
Let's fix that:
app.post("/api/users", (req, res) => {
const { username } = req.body ?? {};
if (typeof username !== "string" || !username.trim()) {
return res.status(400).json({
error: "Username is required"
});
}
const user = {
id: crypto.randomUUID(),
username: username.trim()
};
users.push(user);
res.status(201).json(user);
});
Now invalid requests are rejected before they affect your data.
Note: This example uses an in-memory array. Real applications should use persistent storage and appropriate database constraints.
3. Handle Errors Instead of Crashing
Let's say your API reads data from a file.
app.get("/api/config", (req, res) => {
const config = JSON.parse(
fs.readFileSync("config.json", "utf8")
);
res.json(config);
});
What if config.json doesn't exist or contains invalid JSON?
An exception will be thrown.
Express has error-handling behavior, but you should still decide what response your API should return.
A better approach:
app.get("/api/config", async (req, res) => {
try {
const data = await fs.promises.readFile(
"config.json",
"utf8"
);
res.json(JSON.parse(data));
} catch (error) {
console.error("Failed to load configuration:", error);
res.status(500).json({
error: "Unable to load configuration"
});
}
});
This also avoids exposing internal file paths or stack traces to the client.
4. Authentication Is Not Authorization
These two terms sound similar, but they mean different things.
Authentication: Who are you?
Authorization: Are you allowed to access this resource?
Imagine a user successfully logs into your application.
That doesn't automatically mean they should be able to access every user's private information.
For example:
app.get("/api/users/:id/profile", authenticate, (req, res) => {
if (req.user.id !== req.params.id) {
return res.status(403).json({
error: "Access denied"
});
}
res.json(getProfile(req.user.id));
});
Here, authenticate is middleware that must verify the user's credentials and populate req.user. The example assumes that middleware has already been implemented securely.
The important part is that the API checks which resource the authenticated user is allowed to access.
5. Test the Bad Cases Too
It's easy to test whether an endpoint works when everything goes right.
But some of the most valuable tests are the ones that intentionally send invalid requests.
Node.js even has a built-in test runner.
const { test } = require("node:test");
const assert = require("node:assert/strict");
test("unknown user returns 404", async () => {
const response = await fetch(
"http://localhost:3000/api/users/unknown"
);
assert.equal(response.status, 404);
const body = await response.json();
assert.equal(body.error, "User not found");
});
This test assumes your API server is already running.
You can run it using:
node --test
A few things worth testing:
- Missing or invalid input
- Unknown resources
- Missing authentication
- Insufficient permissions
- Unexpected server errors
Tests like these help catch regressions before users encounter them.
6. Don't Leak Sensitive Information in Logs
Logging is incredibly useful when debugging an API.
But logging everything can create security problems.
For example:
console.log(req.headers);
console.log(req.body);
This might expose passwords, access tokens, or other sensitive information.
Instead, log only what you need:
console.log({
method: req.method,
path: req.path,
timestamp: new Date().toISOString()
});
If you need more detailed logging, make sure sensitive fields are properly redacted.
Final Thoughts
A working API isn't necessarily a reliable API.
Returning the right JSON is just the beginning.
A reliable API should:
- Return meaningful HTTP status codes
- Validate incoming data
- Handle unexpected failures
- Enforce authentication and authorization
- Include tests for failure scenarios
- Avoid exposing sensitive information
You don't need a complicated architecture to start improving API reliability.
Sometimes, a few well-placed checks and tests can make a huge difference.
What's one API mistake you've learned from?
I'd be interested to hear what other developers have encountered.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.