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
validateRequestBody
which 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.body
with 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
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 -
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
TypedRequestBody
we passtypeof todosSchema
now this evaluates toZodType
fromzod
. In Vscode hover overZodType
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 -
<TBody extends ZodType<any, ZodTypeDef, any>>
- Why are we using
ZodType
because we will be usingz.infer<ZodType>
which acceptsZodType
therefore ourTBody
should be of type / should extendZodType
. - Now as you have seen previously the
Request
andRequestHandler
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 expressRequest
where 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)