The body shape in Error Middleware works, but every API invents its own. RFC 9457, Problem Details for HTTP APIs (July 2023, obsoleting RFC 7807), standardizes it: a JSON object served as application/problem+json with five members — type, a URI naming the problem kind; title, a short summary of that kind, stable across occurrences; status, an advisory copy of the HTTP status; detail, an explanation of this occurrence; and instance, a URI identifying the occurrence. Extra members are allowed. Adopting it changes the tail of one function, not your routes.
const P = 'https://example.com/problems/';
const PROBLEMS = {
validation_failed: { type: P + 'validation', title: 'Invalid request body' },
not_found: { type: P + 'not-found', title: 'Resource not found' },
internal_error: { type: 'about:blank', title: 'Internal Server Error' }
};
const code = status >= 500 ? 'internal_error' : err.code;
const { type, title } = PROBLEMS[code] ?? PROBLEMS.internal_error;
res.status(status).type('application/problem+json').json({
type, title, status, instance: req.originalUrl, traceId: req.id,
detail: status >= 500 ? 'The request could not be completed.' : err.message,
...(err.details ? { errors: err.details } : {})
});{ "type": "https://example.com/problems/validation", "title": "Invalid request body",
"status": 422, "instance": "/api/books", "traceId": "8b2f",
"detail": "Request validation failed",
"errors": [ { "in": "body", "field": "year", "code": "too_big",
"message": "Too big: expected number to be <=2026" } ] }title describes the class of problem and detail the instance, which is the distinction most implementations get backwards. Keep title identical for every 422 so a client can switch on it, and put the changing text in detail. type is a stable identifier, not necessarily a live page — clients must not dereference it — but pointing it at real documentation costs nothing. about:blank is correct when the status alone says everything.
The per-field list is an extension member; RFC 9457 defines no standard name, so errors is a convention. Carry your correlation id here too (Correlation IDs): a user who can quote traceId turns an unreproducible bug report into one log query.