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.
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>;
Types of validation
Usually you will have the following architecture to handle Rest API Requests:
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.
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',
});
which will have as output:
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',
}),
},
});
}
}
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': '*',
},
};
};
Which will return a descriptive error message, when we invoke to our API.
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)