Validate TypeScript CDK stack with Zod
Tags: aws, cdk, typescript, zod
Published: 2023-06-18 11:09:50

I have tried to validate CDK stacks written in TypeScript with a validation tool called Zod.

---

## Assumptions

I am using CDK with TypeScript.

I also use v2 CDK.

```sh
❯ cdk --version
2.31.0 (build b67950d)
```

---

## What is Zod?

It is a TypeScript validation tool, [see GitHub](https://github.com/colinhacks/zod).

Zod is a tool that allows you to perform validation based on defined rules at **the timing of parsing object information, such as requests, into objects of a defined type**.

You can write validation rules in the same file as the **type definition**, so it is very easy to perform validation.

```typescript
import { z } from "zod";

export const RequestSchema = z.object({
  title: z.number(),
  message: z.string().min(1),
  mailAddress: z.string().email().min(1),
  tags: z.array(z.string().min(1)).optional(),
});

export type Request = z.infer<typeof RequestSchema>;
```

```typescript
// Validation is performed here at the same time.
const request: Request = RequestSchema.parse(body);
```

As above, not only type, optional (`.optional()`), and empty character check (`.min(1)`), but also **email format handling** (`.email()`) can be performed.

In case of a validation error, the following error is output.

```json
{
  "issues": [
    {
      "validation": "email",
      "code": "invalid_string",
      "message": "Invalid email",
      "path": [
        "mailAddress"
      ]
    }
  ],
  "name": "ZodError"
}
```

---

## Code Examples

Let's start with a code example.

I will take the three main files as examples.

- Zod schema
- Stack class
- Validator class

If you implement validation with the following code, the defined validation will be performed at `cdk synth` time.

### Zod Schema

First, define the parameters you want to validate as a **Zod schema** and define the **validation rules** by Zod for the properties of the schema.

Also, by using `z.infer<typeof StackInputSchema>`, you can define a type from the **schema**, which is useful to use as a type when dealing with stack classes and validator classes.

```typescript
import { z } from "zod";

export const StackInputSchema = z.object({
  topicName: z.string().min(1),
  mailAddress: z.string().email().min(1),
});

export type StackInput = z.infer<typeof StackInputSchema>;

```

### Stack class

This is a simple example. The `init()` is used to validate and the `create()` is used to create the resource.

The `StackInput` type defined earlier is used to define a variable for validation, which is then passed directly to the constructor of the validator.

```typescript
export class SampleStack extends Stack {
  private stackInput: StackInput;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    this.init();
    this.create();
  }

  private init(): void {
    this.stackInput = {
      topicName: props.config.topicName, // Pass parameters from outside the stack via StackProps
      mailAddress: props.config.mailAddress,
    };

    const stackValidator = new StackValidator(this.stackInput);
    this.node.addValidation(stackValidator);
  }

  private create() {
  // ...
```

In `init()`, **create an instance of the validator class**, **pass the information to be validated in the constructor**, and pass the instance to the method `this.node.addValidation`.

This is the validation method provided by CDK, which allows **multiple validations** to be registered, and the validation is run **during cdk synth**.

### Validator class

This is a class to write a concrete validation process, and it performs the validation process **with Zod in** `validate()`.

By **cutting out from the stack class as a validator class** like this, if you stop using Zod and change the validation method, **you do not have to modify the stack class itself**, which has the advantage of making it more maintainable.

```typescript
import { IValidation } from "constructs";
import { StackInput, StackInputSchema } from "../types/stack-input";

export class StackValidator implements IValidation {
  private stackInput: StackInput;

  constructor(stackInput: StackInput) {
    this.stackInput = stackInput;
  }

  public validate(): string[] {
    const errors: string[] = [];

    try {
      StackInputSchema.parse(this.stackInput);
    } catch (e) {
      errors.push(JSON.stringify(e));
    }

    return errors;
  }
}
```

By passing the object to be validated to the parse method of the schema defined in the Zod schema file described below, the parsing and validation are performed together.

If successful, the object will be stored in a variable of the type you also define, and if unsuccessful, a Zod validation error will be thrown.

```typescript
StackInputSchema.parse(this.stackInput);
```

---

## Validation run

Now, let's quickly run `cdk synth` with the email address intentionally formatted improperly (`abcde.fgh`).

```
/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2
  `);throw new Error(`Validation failed with the following errors:
           ^
Error: Validation failed with the following errors:
  [SampleStack] {"issues":[{"validation":"email","code":"invalid_string","message":"Invalid email","path":["senderAddress"]}],"name":"ZodError"}
    at validateTree (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:12)
    at Object.synthesize (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:1:598)
    at App.synth (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/stage.js:1:1866)
    at process.<anonymous> (/Users/goto/github/mail-queues/node_modules/aws-cdk-lib/core/lib/app.js:1:1164)
    at Object.onceWrapper (events.js:520:26)
    at process.emit (events.js:400:28)
    at process.emit (domain.js:475:12)
    at process.emit.sharedData.processEmitHook.installedValue [as emit] (/Users/goto/github/mail-queues/node_modules/@cspotcode/source-map-support/source-map-support.js:745:40)

Subprocess exited with error 1
```

As defined in Zod's schema, it caught the email validation and output an error.

---

## Finally

I wrote [an article on validation with CDK last time](https://dev.to/aws-builders/validation-with-aws-cdk-addvalidation-20lo), and this time I put more focus on TypeScript and included Zod for convenience.

After all, it is easy to leave validation to a framework.