Skip to content

A better way to handle pre-processed bodies by zod / joi / yup / ajv #655

@GerbenRampaart

Description

@GerbenRampaart

I use Zod in a middleware to pre-validate the submitted bodies. However I found no consistent way to handle pre-parsed bodies with just the request.body property. To illustrate, here's how I've solved it.

The Zod middleware:

    import { Context, Next } from 'oak';
    import { AppState } from '../AppState.ts';
    import z from 'zod';
    
    export const zodMw = <TBody>(schema: z.ZodObject<any>) => {
      return async (ctx: Context<AppState<TBody>>, next: Next) => {
	      // First we also checked if the request body was actually json,
	      // But ctx.request.body.json() will just throw a bad request
	      // if it isn't.
	      const body = await ctx.request.body.json();
	      const result = await schema.safeParseAsync(body);
  
	      if (!result.success) {
		      ctx.response.body = result.error.issues;
		      ctx.throw(400, 'Not a valid request body');
	      } else {
		      ctx.state.validatedBody = result.data as TBody;
	      }
  
	      await next();
        };
    };

Let's say I have this Zod schema:

    import { z } from 'zod';
    
    export const v1ExplainPostRequestBodySchema = z.object({
        question: z.string(),
    });
    
    export type V1ExplainPostRequestBodySchema = z.infer<typeof v1ExplainPostRequestBodySchema>;

I can add the Zod middleware before the route:

    return r
	        .post(
		        '/v1/explain',
		        zodMw<V1ExplainPostRequestBodySchema>(v1ExplainPostRequestBodySchema),
		        v1ExplainPost,
	        );`

And then the route looks like this:

    import { Context } from 'oak';
    import { AppState } from '../../../AppState.ts';
    import { V1ExplainPostRequestBodySchema } from './RequestBody.ts';
    import { explain } from '../../../../run/gh.ts';
    
    export const v1ExplainPost = async (ctx: Context<AppState<V1ExplainPostRequestBodySchema>>) => {
        const result = await explain(ctx.state.validatedBody.question);
        ctx.response.body = result;
    };

This works, the body is nicely typed AND validated, and the validation, schema and route handling are nicely separated.

The only thing that bothers me a bit obviously is the line

ctx.state.validatedBody

I don't want to put the validated body on the state object. I want it to be on the request object.

Would it be possible to have maybe a nullable property on the request where the user can store a validated or resolved body?

Currently the place we await the body resolving must also be the place you use it.

Or am I missing a natural way this can already be done?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions