DEV Community

Cover image for How to return meaningful error messages with Zod, Lambda and API Gateway in AWS CDK
Katherine
Katherine

Posted on

How to return meaningful error messages with Zod, Lambda and API Gateway in AWS CDK

In this post you’ll learn how to generate Schemas with Zod and use them to validate incoming requests at the level of API Gateway and Lambda.

Source Code of this project

What is Zod?

Zod is a TypeScript-first schema declaration and validation library. It allows you to define schemas for your data structures, validate that data at runtime, and provides strong type inference for better type safety.

Let’s start with the schema declaration, this is an example of a Zod schema of a User:

import { z } from 'zod';

export const userSchema = z.object({
  name: z.string(), // Name must be a string
  age: z.number().int().min(18), // User's age must be an integer greater than 18
  email: z.string().email(), // Must be a valid email
});

export type UserEventType = z.infer<typeof userSchema>;
Enter fullscreen mode Exit fullscreen mode

Types of validation

Usually you will have the following architecture to handle Rest API Requests:

Image description

For this architecture we can apply two types of validations

Validation at the Level of API Gateway

API Gateway-level validation is powerful because it rejects invalid requests before they reach your downstream services. This prevents unnecessary consumption of Lambda invocations or other resources and saves you money(resources not used = resources not paid).

But the biggest Con is that you can not customize the error message returned by API Gateway’s built-in validation and the schema only supports JSON Schema Draft 4.

Image description

In order to validate the incoming request body at the level of the API Gateway, we need to define our schema using JSON Schema draft 4.
In order to transform our previously User Zod Schema to a valid JSON Schema we are going to use the library zod-to-json-schema.

import { userSchema } from './src/schema/user';
import { zodToJsonSchema } from 'zod-to-json-schema';

const myRequestJsonSchema = zodToJsonSchema(userSchema, {
   target: 'openApi3',
});
Enter fullscreen mode Exit fullscreen mode

which will have as output:

Image description
We use the generated JSON Schema to create the Model to be used as Validator.
On AWS CDK this will look like:

export class CDKRestAPI extends Stack {
  constructor(scope: App, id: string, props: StackProps) {
    super(scope, id, props);

    /* ----------------- Lambda ----------------- */

    const lambdaExample = new NodejsFunction(this, 'LambdaExample', {
      functionName: 'lambda-example',
      runtime: Runtime.NODEJS_20_X,
      entry: path.join(__dirname, './src/lambdaExample.ts'),
      architecture: Architecture.ARM_64,
    });

    // We transform the Zod schema to a valid schema
    const myRequestJsonSchema = zodToJsonSchema(userSchema, {
      target: 'openApi3',
    });

    /* ----------------- API Gateway ----------------- */

    const myAPI = new RestApi(this, 'MyAPI', {
      restApiName: 'myAPI',
    });

    const lambdaIntegration = new LambdaIntegration(lambdaExample);

    myAPI.root.addMethod('POST', lambdaIntegration, {
      // We configure it to validate the request using the Model
      requestValidatorOptions: {
        requestValidatorName: 'rest-api-validator',
        validateRequestBody: true,
      },
      requestModels: {
        'application/json':
          new Model(this, 'my-request-model', {
            restApi: myAPI,
            contentType: 'application/json',
            description: 'Validation model for the request body',
            modelName: 'myRequestJsonSchema',
            schema: myRequestJsonSchema,
          }),
      },
      methodResponses: [
        {
          statusCode: '200',
          responseModels: {
            'application/json': Model.EMPTY_MODEL,
          },
        },
        {
          statusCode: '400',
          responseModels: {
            'application/json': Model.ERROR_MODEL,
          },
        },
        {
          statusCode: '500',
          responseModels: {
            'application/json': Model.ERROR_MODEL,
          },
        },
      ],
    });

    myAPI.addGatewayResponse('ValidationError', {
      type: apigateway.ResponseType.BAD_REQUEST_BODY,
      statusCode: '400',
      templates: {
         // We format the response to include the errors returned from the validation
        'application/json': JSON.stringify({
          errors: '$context.error.validationErrorString',
          details: '$context.error.message',
        }),
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Beyond validation of the body request, you can also validate the presence of headers, query string parameters and path parameters.

Validation at the level of Lambda

Lambda validation is a great tool to validate the schema if you'd like to apply more advanced validation not supported by JSON Draft 4 ( like the conditionals if, then, else supported from JSON Schema Draft 7) or customized the error messages returned to the client.

Using Zod inside the Lambda, you can parse and validate the request body:

import type { Handler } from 'aws-lambda';
import { userSchema } from './schema/user';

export const handler: Handler = async (event) => {
  const body = JSON.parse(event.body);
  const result = userSchema.safeParse(body);
  if (!result.success) {
    const errors = result.error.errors.map((err) => ({
      path: err.path.join('.'),
      message: err.message,
    }));

    return {
      statusCode: 400,
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        message: 'Validation failed',
        errors: errors,
      }),
    };
  }
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'hellou, hellou',
    }),
    headers: {
      'Access-Control-Allow-Headers': 'Content-Type',
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': '*',
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

Which will return a descriptive error message, when we invoke to our API.

Image description

Summary

Having a combination of validation at the Level of API Gateway and a validation at the level Lambda you can protect your APIs while at the same time returning meaningful messages.

Top comments (0)