Skip to content

Instantly share code, notes, and snippets.

@yarabramasta
Created August 2, 2023 16:45
Show Gist options
  • Save yarabramasta/0fbe322bd514f83220258022934788bd to your computer and use it in GitHub Desktop.
Save yarabramasta/0fbe322bd514f83220258022934788bd to your computer and use it in GitHub Desktop.
Contract based Express.js JSON endpoint made with Zod
import { ZodError, type z } from 'zod';
import { type Callback, type Handler, type InputType } from './types';
export default function jsonEndpoint<I, O, E>(
input: {
type: InputType;
schema: z.Schema<I>;
},
output: z.Schema<O>,
cb: Callback<I, O, E>
): Handler<I, E> {
return async (req, res) => {
try {
const parsed = input.schema.parse(req[input.type]);
const value = await cb({ req, res, input: parsed });
const result = output.parse(value);
res.status(200).json(result);
} catch (ex) {
if (ex instanceof ZodError) {
res.status(400).json({ errors: ex.format()['_errors'] });
return;
}
if (process.env.NODE_ENV === 'development') console.error(ex);
res.status(500).json({ error: (ex as Error).message });
}
};
}
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Request, type Response } from 'express';
export type InputType = 'params' | 'query' | 'body';
type TypedRequest<I> = Request<
InputType extends 'params' ? I : any,
unknown,
InputType extends 'body' ? I : any,
InputType extends 'query' ? I : any
>;
export type Context<I, E> = {
req: Omit<Request, 'params' | 'body' | 'query'> & E;
res: Pick<Response, 'setHeader' | 'sendStatus'>;
input: I;
};
export type Callback<I, O, E> = (context: Context<I, E>) => Promise<O>;
export type Handler<I, E> = (
req: TypedRequest<I> & E,
res: Response
) => Promise<void>;
app.get(
'/',
jsonEndpoint(
{ type: 'query', schema: z.object({ name: z.string().optional() }) },
z.object({ message: z.string() }),
async ({ input }) => ({ message: `hello, ${input.name}` })
)
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment