http clients and apis
Status Codes
HTTP status codes tell the client what happened. They are part of the API contract, not decoration.
Good status codes help clients decide whether to retry, ask the user to log in, show validation errors, report a missing resource, or treat a failure as a server problem.
Basic status groups
<?php
declare(strict_types=1);
$status = 404;
echo match ($status) {
200 => 'OK',
201 => 'Created',
400 => 'Bad Request',
404 => 'Not Found',
default => 'Unexpected status',
} . PHP_EOL;
// Prints:
// Not Found
Status code families:
2xx: success3xx: redirect or not modified4xx: client/request problem5xx: server/provider problem
Common success codes
200 OK is the default for successful reads and many updates.
201 Created means a resource was created. It often includes the created resource or a Location header.
204 No Content means the action succeeded and there is no response body.
<?php
declare(strict_types=1);
function successStatusFor(string $action): int
{
return match ($action) {
'create' => 201,
'delete' => 204,
default => 200,
};
}
echo successStatusFor('create') . PHP_EOL;
// Prints:
// 201
Client error codes
400 Bad Request is useful when the request is malformed, such as invalid JSON.
401 Unauthorized means authentication is missing or invalid. The name is confusing, but it is about authentication.
403 Forbidden means the user is authenticated but not allowed.
404 Not Found means the route or resource was not found. Some APIs also use it to avoid revealing private resources.
405 Method Not Allowed means the path exists, but not for that method.
409 Conflict means the request conflicts with current state, such as a version mismatch or duplicate unique value.
422 Unprocessable Content is commonly used for validation errors where the JSON was syntactically valid but fields failed validation.
429 Too Many Requests means rate limiting.
<?php
declare(strict_types=1);
function statusForProblem(string $problem): int
{
return match ($problem) {
'invalid_json' => 400,
'not_logged_in' => 401,
'not_allowed' => 403,
'missing_resource' => 404,
'validation_failed' => 422,
'rate_limited' => 429,
default => 500,
};
}
echo statusForProblem('validation_failed') . PHP_EOL;
// Prints:
// 422
Server errors
500 Internal Server Error means something unexpected failed.
502 Bad Gateway, 503 Service Unavailable, and 504 Gateway Timeout often appear when proxies, upstream services, or infrastructure are involved.
Do not use 500 for validation errors. If the client sent bad data, use a 4xx code.
Be consistent
There is room for judgement in status codes, especially around 400 vs 422 or 404 vs 403. The key is consistency within the API.
Document the convention and follow it. Clients should not need to guess endpoint by endpoint.
What to check in a project
Check status codes match response bodies.
Check validation errors do not return 200.
Check authentication and authorization use distinct statuses.
Check rate limits use 429 and include useful headers where the project supports them.
Check no-content responses use 204 with an empty body.
Check clients handle non-2xx responses deliberately.
What you should be able to do
After this lesson, you should be able to choose common API status codes, distinguish 400 from 422, 401 from 403, and 404 from 405, and explain how status codes guide client behaviour.
Practice
Task: Choose API Status Codes
Write a small PHP script that maps common API outcomes to status codes.
Requirements
- Use
declare(strict_types=1);. - Include outcomes for created, deleted, invalid JSON, validation failure, unauthenticated, forbidden, missing resource, and rate limited.
- Return the status code and a short label.
- Print at least five outcomes.
Check Your Work
Run the script and confirm authentication and authorization are not treated as the same status.
Show solution
This solution keeps the mapping explicit so status-code choices are easy to review.
<?php
declare(strict_types=1);
/**
* @return array{status: int, label: string}
*/
function statusForOutcome(string $outcome): array
{
return match ($outcome) {
'created' => ['status' => 201, 'label' => 'Created'],
'deleted' => ['status' => 204, 'label' => 'No Content'],
'invalid_json' => ['status' => 400, 'label' => 'Bad Request'],
'validation_failed' => ['status' => 422, 'label' => 'Unprocessable Content'],
'unauthenticated' => ['status' => 401, 'label' => 'Unauthorized'],
'forbidden' => ['status' => 403, 'label' => 'Forbidden'],
'missing_resource' => ['status' => 404, 'label' => 'Not Found'],
'rate_limited' => ['status' => 429, 'label' => 'Too Many Requests'],
default => ['status' => 500, 'label' => 'Internal Server Error'],
};
}
foreach (['created', 'deleted', 'validation_failed', 'unauthenticated', 'forbidden', 'rate_limited'] as $outcome) {
$result = statusForOutcome($outcome);
echo $outcome . ': ' . $result['status'] . ' ' . $result['label'] . PHP_EOL;
}
// Prints:
// created: 201 Created
// deleted: 204 No Content
// validation_failed: 422 Unprocessable Content
// unauthenticated: 401 Unauthorized
// forbidden: 403 Forbidden
// rate_limited: 429 Too Many Requests
Why This Works
The examples show successful creation, successful deletion, validation failure, authentication failure, authorization failure, and rate limiting as separate outcomes.