Setup express with Typescript - request body validation with zod
Tags: javascript, typescript, node
Published: 2022-07-12 07:04:54

### Introduction

In this series we will setup an express server using `Typescript`, we will be using `TypeOrm` as our ORM for querying a PostgresSql Database, we will also use `Jest` and `SuperTest` for testing. The goal of this series is not to create a full-fledged node backend but to setup an express starter project using typescript which can be used as a starting point if you want to develop a node backend using express and typescript.

### Overview

This series is not recommended for beginners some familiarity and experience working with `nodejs`, `express`, `typescript` and `typeorm` is expected. In this post which is part six of our series we will : -
 - Add zod validations for todos.
 - Implement zod-express-middleware on our own.
 - Add the validation middleware to our routes.

### Step One: Zod validations

Lets first install zod in our project by running - 
```bash
npm install --save zod
```
Then under `src/api/todos` create a new file `todos.validation.ts` and paste the following code : 
```ts
import { z } from 'zod';

import { TodoStatus } from './todos.entity';

export const todosSchema = z.object({
  text: z.string({
    invalid_type_error: 'todo text should be a string',
    required_error: 'todo text is required',
  }),
  status: z.enum([TodoStatus.PENDING, TodoStatus.DONE], {
    errorMap() {
      return {
        message: 'todo status must be pending | done',
      };
    },
  }),
});

export const todoGetSchema = z.object({
  todoId: z
    .string({
      required_error: 'todoId is required',
    })
    .uuid({
      message: 'todo id should be uuid',
    }),
});
``` 
The above code is self-explanatory, if you are new to zod, please check their github page, there you can find the docs related to each method we used above. 

Now lets create a new Error class, under `src/utils` create a file `ValidationError.ts` and paste the following - 
```ts
import { BaseError } from './BaseError';

export class ValidationError extends BaseError {
  status: number;
  validationErrors: unknown;

  constructor(message: string, status: number, validationErrors: unknown) {
    super(message);
    this.status = status;
    this.validationErrors = validationErrors;
  }
}
```

Now lets handle our validation errors in the `globalErrorHandler` function in the `server.ts` - 
```ts
 private globalErrorHandler() {
    this.app.use(
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      (error: Error, req: Request, res: Response, next: NextFunction) => {
        console.log('Error (Global Error Handler)', error.stack);
        // Handle 404 not found routes
        if (error instanceof NotFoundError) {
          return res.status(error.status).json({
            status: false,
            statusCode: error.status,
            message: error.message,
          });
        }

       // Handle zod request body, params validation
        if (error instanceof ValidationError) {
          return res.status(error.status).json({
            status: false,
            statusCode: error.status,
            message: error.message,
            errors: error.validationErrors,
          });
        }

        // Handling internal server errors
        return res.status(500).json({
          status: false,
          statusCode: 500,
          message: 'Something unusual Happened',
        });
      }
    );
  }
```

### Step Two: Setup zod validation middleware

`zod-express-middleware` is an awesome library for a validation middleware. The only issue is this library sends responses directly from the middleware, I want to handle all the errors in the `globalErrorHandler` instead. To do so we need to `throw` our errors from the middleware. So what do we do ? Well we read the code of this library form github and implement it on our own. Now this section is a bit complex bear with me, you need to read and re-read the code to understand what we are doing if you are not too familiar with TypeScript generics and reading types of external libraries. 

Under `src/middlewares` create a new file `validate.ts` - 
```ts
import { Request, RequestHandler } from 'express';
import { ParamsDictionary } from 'express-serve-static-core';
import { ZodSchema, ZodTypeDef, ZodType, z } from 'zod';

import { ValidationError } from '../utils/ValidationError';

export const validateRequestBody: <TBody>(
  zodSchema: ZodSchema<TBody>
) => RequestHandler<ParamsDictionary, any, TBody, any> =
  (schema) => (req, res, next) => {
    const parsed = schema.safeParse(req.body);
    if (parsed.success) {
      return next();
    } else {
      const { fieldErrors } = parsed.error.flatten();
      const error = new ValidationError('error validating request body', 422, [
        { type: 'Body', errors: fieldErrors },
      ]);
      throw error;
    }
 };
```

- We created a new function `validateRequestBody` which will validate our request body, just forget the typescript types. This function is a curried function - 
```js
const validateRequestBody = (schema) => (req, res, next) => {}
```
- In the function we are validating our `req.body` with the zod schema passed. If parsing results in error we are throwing a Validation error which will be handled in the `globalErrorHandler`.

Similarly we will create 2 functions one to validate request params and other to validate request query params - 
```ts
export const validateRequestParams: <TParams>(
  zodSchema: ZodSchema<TParams>
) => RequestHandler<TParams, any, any, any> = (schema) => (req, res, next) => {
  const parsed = schema.safeParse(req.params);
  if (parsed.success) {
    return next();
  } else {
    const { fieldErrors } = parsed.error.flatten();
    const error = new ValidationError('error validating request params', 422, [
      { type: 'Params', errors: fieldErrors },
    ]);
    throw error;
  }
};

export const validateRequestQuery: <TQuery>(
  zodSchema: ZodSchema<TQuery>
) => RequestHandler<ParamsDictionary, any, any, TQuery> =
  (schema) => (req, res, next) => {
    const parsed = schema.safeParse(req.query);
    if (parsed.success) {
      return next();
    } else {
      const { fieldErrors } = parsed.error.flatten();
      const error = new ValidationError(
        'error validating request query params',
        422,
        [{ type: 'Query', errors: fieldErrors }]
      );
      throw error;
    }
  };
```

Now lets understand `RequestHandler` type in vscode hover over the type you will se the following - 
```ts
interface RequestHandler<P = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = qs.ParsedQs, Locals extends Record<string, any> = Record<string, any>>
```
It accepts 5 parameters, the first four matter to us: - 
- first is the params `P`
- second response body `ResBody` which is any by default 
- third request body `ReqBody` again any by default 
- fourth request query params `ReqQuery`

In all our above validation functions we use Generics like `TBody`, `TQuery` notice we are passing them right where RequestHandler expects them. 
So for the validateRequestBody which validates the body `ReqBody`, `TBody` is passed as third param to `RequestHandler`.
Similarly for validateRequestParams which validates the request params `P`, `TParams` is passed as the first param to `RequestHandler`. 

Whats the benefit ? The Benefit of this is you will get typed request body, params if you use our middleware like so - 
```ts
this.router.get('/', validateRequestBody(todosSchema), (req, res) => {})
```

But you may ask we won't benefit from this right because we are using controller functions and that too wrapped inside our `asyncHandler` like - 
```ts
this.router.post(
  '/',
   validateRequestBody(todosSchema),
   asyncHandler(todosController.createTodo)
);
```

Well for this `express-zod-middleware` has some extra types to type your request body, params and query params inside `src/utils/validate.ts` file paste the following - 
```ts
export type TypedRequestBody<TBody extends ZodType<any, ZodTypeDef, any>> =
  Request<ParamsDictionary, any, z.infer<TBody>, any>;

export type TypedRequestParams<TParams extends ZodType<any, ZodTypeDef, any>> =
  Request<z.infer<TParams>, any, any, any>;

export type TypedRequestQuery<TQuery extends ZodType<any, ZodTypeDef, any>> =
  Request<ParamsDictionary, any, any, z.infer<TQuery>>;
```

Lets dissect the above types - 
- First we will use the above types like so - 
```ts
(req: TypedRequestBody<typeof todosSchema>, res: Response)
```
- To `TypedRequestBody` we pass `typeof todosSchema` now this evaluates to `ZodType` from `zod`. In Vscode hover over `ZodType` it is a generic with 3 arguments first and third are default any, and second one accepts ZodType. That is how we have typed it here - 
```ts
<TBody extends ZodType<any, ZodTypeDef, any>>
```
- Why are we using `ZodType` because we will be using `z.infer<ZodType>` which accepts `ZodType` therefore our `TBody` should be of type / should extend `ZodType`.
- Now as you have seen previously the `Request` and `RequestHandler` types in express receive 5 arguments we are interesed in first 4 arguments where first is params, second is response (default any), third is request (default any) and fourth is query params.
- In the above code we have used `z.infer<>` in the right places. To type our request body we used it in the third place, for query params we used it in the fourth place. 
- So the `TypedRequestBody` should return a type of express `Request` where the request body is typed according to our zodSchema like so - 
```ts
Request<ParamsDictionary, any, z.infer<TBody>, any>;
```

Now we can use the above types in our controllers like so - 
```ts
async createTodo(req: TypedRequestBody<typeof todosSchema>, res: Response) {
 // our request body will be typed in accordance with todosSchema
  const { text, status } = req.body;
}
```

`express-zod-middleware` also has one handy function `validateRequest` where you can pass 3 schemas one for request body, one for request params and last for request params query and it will validate all of them - 
```ts
export type RequestValidation<TParams, TQuery, TBody> = {
  params?: ZodSchema<TParams>;
  query?: ZodSchema<TQuery>;
  body?: ZodSchema<TBody>;
};

export const validateRequest: <TParams = any, TQuery = any, TBody = any>(
  schemas: RequestValidation<TParams, TQuery, TBody>
) => RequestHandler<TParams, any, TBody, TQuery> =
  ({ params, query, body }) =>
  (req, res, next) => {
    const errors = [];
    if (params) {
      const parsed = params.safeParse(req.params);
      if (!parsed.success) {
        errors.push({
          type: 'Params',
          errors: parsed.error.flatten().fieldErrors,
        });
      }
    }
    if (query) {
      const parsed = query.safeParse(req.query);
      if (!parsed.success) {
        errors.push({
          type: 'Query',
          errors: parsed.error.flatten().fieldErrors,
        });
      }
    }
    if (body) {
      const parsed = body.safeParse(req.body);
      if (!parsed.success) {
        errors.push({
          type: 'Body',
          errors: parsed.error.flatten().fieldErrors,
        });
      }
    }
    if (errors.length > 0) {
      const error = new ValidationError(
        'error validating request',
        422,
        errors
      );
      throw error;
    }
    return next();
  };
```

### Step 3: Use the request validation middlewares

Lets now use our validation middlewares, in `todos.router.ts` - 
```ts
import { Router } from 'express';

import { asyncHandler } from '../../middlewares/asyncHandler';
import {
  validateRequestBody,
  validateRequestParams,
} from '../../middlewares/validate';
import { BaseRouter } from '../../utils/BaseRouter';
import { todosController } from './todos.controller';
import { todosSchema, todoGetSchema } from './todos.validation';

class TodosRouter extends BaseRouter {
  constructor() {
    super();
  }

  addRoutes(): void {
    this.router.get('/', asyncHandler(todosController.getAllTodos));
    this.router.post(
      '/',
      validateRequestBody(todosSchema),
      asyncHandler(todosController.createTodo)
    );
    this.router.get(
      '/:todoId',
      validateRequestParams(todoGetSchema),
      asyncHandler(todosController.getTodoById)
    );
  }

  returnApiEndpointRouter(): Router {
    this.addRoutes();
    return this.router;
  }
}

export const todosRouter = new TodosRouter().returnApiEndpointRouter();
```

And finally lets type our requests in the controller function - 
```ts
import { Request, Response } from 'express';

import {
  TypedRequestBody,
  TypedRequestParams,
} from '../../middlewares/validate';
import { todosService } from './todos.service';
import { todosSchema, todoGetSchema } from './todos.validation';

class TodoController {
  async getAllTodos(req: Request, res: Response) {
    const todos = await todosService.getAllTodos();

    return res.status(200).json({
      status: true,
      statusCode: 200,
      todos,
    });
  }

  async getTodoById(
    req: TypedRequestParams<typeof todoGetSchema>,
    res: Response
  ) {
    const { todoId } = req.params;
    const todo = await todosService.getTodoById(todoId);
    if (!todo) {
      return res.status(404).json({
        status: false,
        statusCode: 404,
        message: `todo not found for id - ${todoId}`,
      });
    }

    return res.status(200).json({
      status: true,
      statusCode: 200,
      todo,
    });
  }

  async createTodo(req: TypedRequestBody<typeof todosSchema>, res: Response) {
    const { text, status } = req.body;
    const newTodo = await todosService.createTodo(text, status);

    return res.status(201).json({
      status: true,
      statusCode: 201,
      todo: newTodo.raw,
    });
  }
}

export const todosController = new TodoController();
```

And with that we come to an end of this tutorial, I agree it was intense. I also feel that many of you might not understand the typescript types for our validations, please feel free to ask questions. If I wanted I could have pulled `express-zod-middleware` directly but reading open source I learn a lot and I hope you did so.

### Overview 
 All the code for this tutorial can be found under the `feat/request-validation` branch [here](https://github.com/yaldram/express-typescript-starter/tree/feat/request-validation). In the next tutorial we will setup basic testing using jest and supertest until next time PEACE.