Skip to content

Uses zod to define well typed API, generate a router that enforces and validates the api at runtime, & generate a well-typed client

License

Notifications You must be signed in to change notification settings

Skutopia-org/zod-http-schemas

Repository files navigation

Zod Http Schemasnpm monthly downloads current version Commitizen friendly

zod-http-schemas brings together the best parts of http-schemas and zod into one library that:

  • Declares the 'shape' of an HTTP API both at compile time and at runtime.
  • Supports transformations and refinements.

At compile time it statically checks:

  • HTTP requests in the client code
  • Route handlers in the server code against the declared schema, ensuring usage errors are caught early.

At runtime, zod-http-schemas:

  • validates that response and request payloads match the declared schema
  • Trims response payloads of any excess properties, preventing information leaks

Use a shared schema for both client and server side code as a single source of truth, ensuring the client and server always agree on the API.

zod-http-schemas uses the zod library for specifying and enforcing schema types.

Installation

npm install zod-http-schemas

Example Shared Code (use in both client and server)

import {createHttpSchema} from 'zod-http-schemas';
import * as z from 'zod';

// Declare the http schema to be used by both client and server
export const apiSchema = createHttpSchema({
    'POST /sum': {
        requestBody: z.array(z.number()),
        responseBody: z.number(),
    },
    'GET /greet/:name': {
        responseBody: z.string(),
    },
});

Example Client-Side Code

import {createHttpClient} from 'zod-http-schemas/client';
import {apiSchema} from '<path-to-shared-schema>';

// Create a strongly-typed http client. These are cheap to create - it's fine to have many of them.
const client = createHttpClient(apiSchema, {baseURL: '/api'});

// Some valid request examples
let res1 = client.post('/sum', {body: [1, 2]});                 // res1: Promise<number>
let res2 = client.get('/greet/:name', {params: {name: 'Bob'}}); // res2: Promise<string>

// Some invalid request examples
let res3 = client.get('/sum', {body: [1, 2]});                  // tsc build error & runtime error
let res4 = client.post('/sum', {body: 'foo'});                  // tsc build error & runtime error
let res5 = client.post('/blah');                                // tsc build error & runtime error

Client-side implementation

zod-http-schemas uses Axios under the hood. Use the same config options with createHttpClient as you would with Axios.

However zod-http-schemas uses its own default validateStatus option that will only reject status codes >= 500. This lets you include common error responses in your schema, without losing typing.

For example, for a post endpoint you might specify

export const apiSchema = createHttpSchema({
    'POST /article': {
        requestBody: NewArticle,
        responseBody: z.union([Article, MyGenericApiErrorType]),
    },
});

Now your clientside code might have a type-guard function that asserts:

import {AxiosResponse} from "axios";

export const isNotErrorResponse = <T, E>(
    response: AxiosResponse<T> | AxiosResponse<MyGenericApiErrorType>
): response is AxiosResponse<T> => {
    return response.status < 400;
};

and used as such:

const result = await apiClient.post('/article', {body: {title: 'Hello world'}});
if (isNotErrorResponse(result)) {
    // result is AxiosResponse<Article> in here
    console.log(result.body);
} else {
    // Some error occured, so result is typed AxiosResponse<MyGenericApiErrorType>
    console.error(result.body.myGenericErrorProperty)
}

Example Server-Side Code

import * as express from 'express';
import {createRequestHandler, decorateExpressRouter} from 'http-schemas/server';
import {apiSchema} from '<path-to-shared-schema>';

// Create a strongly-typed express router.
const apiRouter = decorateExpressRouter({schema: apiSchema});

// Create a normal express app and mount the strongly-typed router.
const app = express();
app.use(express.json()); // it's a normal express app; mount whatever middleware you want
app.use('/api', apiRouter); // `apiRouter` is just middleware; mount it wherever you want

// Add a request handler directly to the router
apiRouter.post('/sum', (req, res) => {
    let result = req.body.reduce((sum, n) => sum + n, 0);
    res.send(result);
});

// Declare a request handler separately, then add it to the router
const greetHandler = createRequestHandler(apiSchema, 'GET', '/greet/:name', (req, res) => {
    res.send(`Hello, ${req.params.name}!`);
});
apiRouter.get('/greet/:name', greetHandler);

// Some invalid route handler examples
apiRouter.post('/blah', (req, res) => {/*...*/});           // tsc build error & runtime error
apiRouter.post('/sum', (req, res) => { req.body.foo[0] });  // tsc build error & runtime error
apiRouter.post('/sum', (req, res) => { res.send('foo') });  // tsc build error & runtime error

app.listen(8000);

Full production-like webserver demo

The best way to see http-schemas in action is to see it in a real demonstration with documentation. Take a look at http-schemas-webserver-demo, read the docs, run it and play with it yourself.

About

Uses zod to define well typed API, generate a router that enforces and validates the api at runtime, & generate a well-typed client

Resources

License

Stars

Watchers

Forks

Packages

No packages published