Node.js and RFC compliant error message
When developing interfaces, the question arises: “How do I handle errors?” My answer is: “As centrally as possible and in accordance with standards.” I'll show you which approach has proven successful for me in the Node.js environment. Let's get started!
In this article, I demonstrate my approach to outputting interface errors of the “Invalid Parameter” category in a standards-compliant manner. For this, I adhere to RFC 7807, which specifies that problem details are transported in a machine-readable HTTP response. The prerequisites for this are the setup of Node.js with TSOA, the type definition in Node.js with TSOA including request body validation, and the implementation of my base error class.
The following steps are necessary for implementation:
- Defining the appropriate class for the “InvalidParams” error.
- Mapping the error classes into an RFC-compliant problem document.
- Creating error middleware for Express.
- Integrating it into the Express application.
1. The InvalidParams Error
We provide detailed information about the invalid parameters so that we can later display in the UI which input caused a problem (e.g., in the form of an error message and a red border around the input field).
// Defining the structure of validation errors in analogy to TSOA
export interface ValidationErrors {
[name: string]: {
message: string // The message shown in the frontend
received?: unknown // The value we received in the API, if available
expected?: unknown // The value we expected, if given
code?: string // To give a hint about what we expect as a machine-readable value
}
}
// Extending BaseError with the newly defined structure
interface InvalidParamsErrorOptions extends BaseErrorOptions {
validationResult: ValidationErrors
}
The new class extends the BaseError according to the type definition mentioned above.
class InvalidParamsError extends BaseError {
public validationResult: ValidationErrors
constructor(options: InvalidParamsErrorOptions) {
const { detail = 'Invalid parameters', cause, validationResult } = options
super({ detail, cause, statusCode: 422 })
this.validationResult = validationResult
}
public toJSON() {
return {
...super.toJSON(),
validationResult: this.validationResult,
}
}
public toString() {
return JSON.stringify(this.toJSON())
}
}
2. Mapping to an RFC-Compliant Problem Document
Two suitable NPM packages are available for basic functionality:
- NPM package http-problem-details provides a
ProblemDocument. It generates the necessaryproblem+jsondocument. - NPM package http-problem-details-mapper allows us to build mapping middleware based on the
ProblemDocument.
npm install http-problem-details http-problem-details-mapper
Using these two NPM packages, we define the mapping to the problem document. We ensure that the status code and error message are written in the status and detail fields, respectively.
import { ProblemDocument } from 'http-problem-details'
import { ErrorMapper } from 'http-problem-details-mapper'
export class InvalidParamsMapper extends ErrorMapper {
mapError(error: InvalidParamsError) {
return new ProblemDocument(
{
status: error.statusCode,
title: 'Invalid parameter',
type: 'validation-error',
detail: error.detail,
},
{
invalidParameter: error.validationResult,
},
)
}
}
3. The necessary error middleware implementations
We still need two components to integrate this into the Express application. Firstly, the component that converts the TSOA error into our InvalidParams error class.
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'
import { ValidateError } from 'tsoa'
import InvalidParamsError from '@/error/InvalidParamsError'
const validationErrorMiddleware: ErrorRequestHandler = (
err: unknown,
_req: Request,
_res: Response,
next: NextFunction,
) => {
if (err instanceof ValidateError) {
next(new InvalidParamsError({ detail: 'Validation failed', validationResult: err.fields }))
}
next(err)
}
export default validationErrorMiddleware
Secondly, the component that converts the error class into the desired HTTP response.
import { ErrorRequestHandler, NextFunction, Request, Response } from 'express'
import { IMappingStrategy } from 'http-problem-details-mapper'
interface HttpProblemResponseOptions {
strategy: IMappingStrategy
}
function HttpProblemResponse(options: HttpProblemResponseOptions): ErrorRequestHandler {
const { strategy } = options
return function HttpProblemDetailsMiddleware(
error: Error,
_request: Request,
response: Response,
next: NextFunction,
): void {
const problem = strategy.map(error)
// in case we cannot map the error, we will not handle it
if (!problem) {
next(error)
return
}
response.statusCode = problem.status
response.setHeader('Content-Type', 'application/problem+json')
response.json(problem)
}
}
export default HttpProblemResponse
4. Integration into the Express Application
As a final step, we integrate all components into the Express application:
- We create the mapping for the InvalidParams error mapper so that it translates the Error class to
ProblemDocument. - We include the
validationErrorMiddleware, which converts the TSOA error into an InvalidParams error, enabling the mapping to take effect. - We activate the mapper's translation.
// Set up mapping to ensure translation to ProblemDocument
const mapperRegistry = new MapperRegistry()
.registerMapper(new InvalidParamsMapper())
// here you register other errors you want to map
const ErrorMappingStrategy = new DefaultMappingStrategy(mapperRegistry)
// This translates the tsoa validation error into an InvalidParamsError,
// which gets resolved into an RFC error response in the last handler
app.use(validationErrorMiddleware)
// The RFC 7807 error middleware has to be the last app.use() to work as intended
// It will map the thrown errors to the RFC applying responses
app.use(HttpProblemResponse({ strategy: ErrorMappingStrategy }))
Conclusion
This article demonstrates how several components are combined to achieve the goal. With this structure, we built the platform to define further errors and centrally translate them into an RFC-compliant HTTP response. Examples include UnauthenticatedError and UnauthorizedError, which I will discuss in subsequent articles on Node.js + TSOA + authentication. Similarly, there's the NotFoundError, which is important with Prisma.
If you want to know how I apply all of this in a project and how I organize my code, then
Let's look at the code together