You deploy an AI-generated APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. and your frontend error tracking shows zero errors. Looks great, until you realize the API returns 200 OK with { "error": "User not found" } for missing users. Your error tracker only counts non-200 responses. The API has been silently failing for weeks.
Reading the request
The request object is where all incoming data lives. Express parses URLs and headers automatically, but body parsing requires middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it..
Route parameters
// URL: GET /users/42/posts/7
app.get('/users/:userId/posts/:postId', (req, res) => {
console.log(req.params);
// { userId: "42", postId: "7" }
});Remember: all params are strings. Always convert them when your database expects numbers.
const userId = parseInt(req.params.userId, 10);
if (isNaN(userId)) {
return res.status(400).json({ error: 'Invalid user ID' });
}Query strings
// URL: GET /products?category=electronics&minPrice=100&inStock=true
app.get('/products', (req, res) => {
console.log(req.query);
// { category: "electronics", minPrice: "100", inStock: "true" }
});Everything in req.query is a string too. "true" is not the boolean true, and "100" is not the number 100.
if (req.query.inStock) to check a boolean query parameter. But req.query.inStock is the string "false", and the string "false" is truthy in JavaScript. So if ("false") evaluates to true. AI almost never handles boolean query parameters correctly.Request body
The body is only available if you register express.json() middleware first.
app.use(express.json());
app.post('/users', (req, res) => {
const { name, email, age } = req.body;
// Only populated if Content-Type is application/json
});| Source | How to access | Type | When to use |
|---|---|---|---|
| URL path | req.params.id | Always a string | Identify a specific resource |
| Query string | req.query.sort | Always a string | Filtering, sorting, pagination |
| Body | req.body.name | Parsed JSON (any type) | Creating or updating resources |
| Headers | req.headers['content-type'] | Always a string | Authentication tokens, content type |
Validating request data
AI-generated handlers almost never validate input. They destructure req.body and pass values straight to the database.
// AI-generated (no validation)
app.post('/users', (req, res) => {
const { name, email } = req.body;
db.createUser(name, email); // name could be undefined, email could be 5000 chars
res.json({ success: true });
});
// What it should look like
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return res.status(400).json({ error: 'Name is required' });
}
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Valid email is required' });
}
db.createUser(name.trim(), email.toLowerCase());
res.json({ success: true });
});Sending responses
Express gives you several methods to send data back to the client.
// Send JSON (most common for APIs)
res.json({ user: { id: 1, name: 'Alice' } });
// Send with a specific status code
res.status(201).json({ user: { id: 1, name: 'Alice' } });
// Send plain text
res.send('Hello world');
// Send nothing (useful for DELETE)
res.status(204).send();
// Redirect
res.redirect('/new-location');| Method | What it does | Content-Type |
|---|---|---|
res.json(data) | Sends JSON, sets Content-Type automatically | application/json |
res.send(data) | Sends string, Buffer, or object | Varies |
res.status(code) | Sets HTTP status code (chainable) | N/A |
res.redirect(url) | Sends 302 redirect | N/A |
res.sendStatus(code) | Sets status and sends status text as body | text/plain |
Response headers
// Set a custom header
res.set('X-Request-Id', '12345');
// Set cache control
res.set('Cache-Control', 'public, max-age=3600');
// Multiple headers at once
res.set({
'X-Request-Id': '12345',
'X-RateLimit-Remaining': '99'
});HTTPWhat is http?The protocol browsers and servers use to exchange web pages, API data, and other resources, defining how requests and responses are formatted. status codes
Status codes are a three-digit number that tells the client what happened. They are grouped into families.
| Family | Range | Meaning | Example |
|---|---|---|---|
| 2xx | 200-299 | Success | Request completed as expected |
| 3xx | 300-399 | Redirection | Resource moved, follow the new URL |
| 4xx | 400-499 | Client error | The request is wrong (bad input, unauthorized) |
| 5xx | 500-599 | Server error | The server failed (bug, database down) |
Status codes you will use constantly
| Code | Name | When to use | Example |
|---|---|---|---|
200 | OK | Successful GET or PUT | Return a list of users |
201 | Created | Successful POST that created a resource | New user created |
204 | No Content | Successful DELETE (nothing to return) | User deleted |
400 | Bad Request | Client sent invalid data | Missing required field |
401 | Unauthorized | Missing or invalid authentication | No token provided |
403 | Forbidden | Authenticated but not permitted | Regular user accessing admin route |
404 | Not Found | Resource does not exist | GET /users/999 when user 999 is not in the database |
409 | Conflict | Request conflicts with current state | Trying to create a user with an email that already exists |
422 | Unprocessable Entity | Valid format but semantically wrong | Age is -5 |
500 | Internal Server Error | Unhandled bug in your code | Database query throws an exception |
The AI status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke. problem
AI-generated APIs overwhelmingly default to status 200 for everything. This is not hypothetical, it is the single most consistent pattern in AI-generated Express code.
// AI-generated - returns 200 for errors
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) {
res.json({ error: 'User not found' }); // Status 200!
}
res.json(user);
});
// Correct - uses proper status codes
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});res.json({ error: '...' }) without setting a status code. The default is 200. This means your monitoring tools report 100% success, your frontend catch blocks never fire (they check for non-2xx responses), and errors go completely unnoticed in production.Why wrong status codes break things
Status codes are not just for humans reading logs. Automated systems depend on them.
| System | What it does with status codes |
|---|---|
Frontend fetch() | Only rejects on network errors, not 4xx/5xx, but response.ok is false for non-2xx, so client-side error handling relies on correct codes |
| Browser caching | 200 responses can be cached; 500 responses are not, returning 200 for errors caches error responses |
| Load balancers | 5xx responses trigger health checks and routing changes |
| Monitoring (Datadog, etc.) | Dashboards count 4xx and 5xx as errors, 200 with error body is invisible |
| API consumers | Other services parse status codes to decide retry logic, retry on 503, do not retry on 400 |
Response patterns for CRUDWhat is crud?Create, Read, Update, Delete - the four basic operations almost every application performs on data. operations
Here is what a complete CRUD APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. should return for each operation.
// CREATE - 201 with the created resource
app.post('/books', (req, res) => {
const book = db.createBook(req.body);
res.status(201).json(book);
});
// READ (single) - 200 or 404
app.get('/books/:id', (req, res) => {
const book = db.findBook(req.params.id);
if (!book) return res.status(404).json({ error: 'Book not found' });
res.json(book);
});
// READ (list) - 200 with array (empty array is fine, not 404)
app.get('/books', (req, res) => {
const books = db.findBooks(req.query);
res.json(books); // [] is a valid, successful response
});
// UPDATE - 200 with updated resource
app.put('/books/:id', (req, res) => {
const book = db.updateBook(req.params.id, req.body);
if (!book) return res.status(404).json({ error: 'Book not found' });
res.json(book);
});
// DELETE - 204 with no body
app.delete('/books/:id', (req, res) => {
const deleted = db.deleteBook(req.params.id);
if (!deleted) return res.status(404).json({ error: 'Book not found' });
res.status(204).send();
});200 for successful POST requests instead of 201. It also returns 200 with the deleted object for DELETE instead of 204 with no body. These are not catastrophic, but they confuse API consumers and violate the conventions that every REST client expects.