The Problem API RFC
In the world of modern web development and API design, creating robust and user-friendly applications requires not only handling successful responses but also effectively managing errors. When errors occur in API interactions, providing clear, consistent, human, and machine-readable error responses becomes paramount.
In the past I have personally created error payloads that look like the following:
{
"success": false,
"message": "Some error"
}
Or maybe even
{
"status": 500,
"error": "Some error"
}
This might work for some issues but eventually we'll change it, either between projects or within the same one.
The solution has the very catchy name RFC 7807: Problem Details for HTTP APIs, it was released in 2016, here's an example response from it:
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
It's extremely powerful, ensuring a unique type identifier, human readable parameters, and programmaticly readable data. These errors make it easy to implement error feedback for users, either through toasts or message boxes.
Laravel makes this sort of standard very easy to implement using the exception handler to render a custom class that extends Exception and implements Responsable.
We can then use it to replace the existing Laravel errors:
// app/Exceptions/Handler.phppublic function register(): void
{
$this->renderable(function (HttpExceptionInterface $e, Request $request) {
// Make sure to use toResponse as other middleware assume it to be a response!
return (new HttpApiProblem($e->getStatusCode(), $e->getMessage(), previous: $e))->toResponse($request);
});
$this->renderable(function (AuthenticationException $e, Request $request) {
return (new HttpApiProblem(401, $e->getMessage(), previous: $e))->toResponse($request);
});
$this->renderable(function (ValidationException $e, Request $request) {
return (new ValidationApiProblem($e))->toResponse($request);
});
}
By using TypeScript in our frontend we can make our errors typesafe!
export const API_PROBLEM_TYPES = {
HTTP: 'https://www.rfc-editor.org/rfc/rfc9110.html#name-status-codes',
PASSWORD_VALIDATION_REQUIRED: 'https://evba.uk/errors/password-validation-required',
VALIDATION: 'https://laravel.com/errors/validation',
} as const
type BaseApiProblem<Type extends string, Data = unknown> = Data & {
status: number
type: Type
title: string
detail: string
}
type HttpProblem = BaseApiProblem<typeof API_PROBLEM_TYPES.HTTP>
type PasswordValidationRequiredProblem = BaseApiProblem<typeof API_PROBLEM_TYPES.PASSWORD_VALIDATION_REQUIRED>
export type ValidationProblem<TParams extends string = string> = BaseApiProblem<
typeof API_PROBLEM_TYPES.VALIDATION,
{
errors: { [Key in TParams]: string[] }
}
>
export type ApiProblem<TParams extends string = string> =
| HttpProblem
| PasswordValidationRequiredProblem
| ValidationProblem<TParams>
We can then type narrow these using a type predicate
export function isApiProblem(data: unknown, res: Response): data is ApiProblem {
return res.headers.get('Content-Type') === 'application/problem+json'
}
// Or...
export function isApiProblem(response: unknown): response is ApiProblem {
return (
typeof response === 'object' &&
!!response &&
'status' in response &&
'type' in response &&
'title' in response &&
'detail' in response
)
}
Once you know it's one of the many API problems you can use the type property to narrow it further.