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 -
npm install --save zod
Then under src/api/todos create a new file todos.validation.ts and paste the following code :
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 -
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 -
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 -
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
validateRequestBodywhich will validate our request body, just forget the typescript types. This function is a curried function -
const validateRequestBody = (schema) => (req, res, next) => {}
- In the function we are validating our
req.bodywith the zod schema passed. If parsing results in error we are throwing a Validation error which will be handled in theglobalErrorHandler.
Similarly we will create 2 functions one to validate request params and other to validate request query params -
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 -
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
ResBodywhich is any by default - third request body
ReqBodyagain 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 -
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 -
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 -
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 -
(req: TypedRequestBody<typeof todosSchema>, res: Response)
- To
TypedRequestBodywe passtypeof todosSchemanow this evaluates toZodTypefromzod. In Vscode hover overZodTypeit 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 -
<TBody extends ZodType<any, ZodTypeDef, any>>
- Why are we using
ZodTypebecause we will be usingz.infer<ZodType>which acceptsZodTypetherefore ourTBodyshould be of type / should extendZodType. - Now as you have seen previously the
RequestandRequestHandlertypes 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
TypedRequestBodyshould return a type of expressRequestwhere the request body is typed according to our zodSchema like so -
Request<ParamsDictionary, any, z.infer<TBody>, any>;
Now we can use the above types in our controllers like so -
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 -
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 -
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 -
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. In the next tutorial we will setup basic testing using jest and supertest until next time PEACE.
Top comments (0)