npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

express-ts-handler

v0.0.1

Published

Express route handler for type-safe validations

Downloads

172

Readme

express-ts-handler

This is route wrapper for Express that enables:

  • preserving correct types of query, params, and body after validation
  • type checking response, TS error when leaking extra data such as password
  • validating response when not in production
  • response may be returned from the handler
  • fixes async error handling problem of Express
  • middlewares can change the type of req on the fly

Supports Zod for validations. It doesn't depend on validation library directly, so it could be integrated with other validation libraries in future.

Check out example code.

Get started

npm i express-ts-handler zod

First, we need to initialize a handler function:

import { makeHandler } from 'express-ts-handler';
import { z, ZodSchema } from 'zod';

export const handler = makeHandler<ZodSchema>({
  parse: (type, value) => type.parse(value),
  object: z.object,
})

Validating query, params, body

When we don't specify any validations, req.query, req.params, and req.body become of type unknown:

app.post('/path/:id',
  handler({
    handler(req) {
      // no error in default Express, type error with this library:
      req.body?.name.toLowerCase();
    },
  }),
);

Before we can use any request data, we need to add a validation for it. All unknown keys will be stripped by Zod, so we can safely save req.body to a database.

app.post('/path/:id',
  handler({
    // coerce and validate id from route path
    params: {
      id: z.coerce.number().int(),
    },
    // validate query string
    query: {
      key: z.string().optional(),
    },
    // validate request body
    body: {
      name: z.string(),
    },
    // finally, route handler. It may be sync or async
    async handler(req) {
      // all the data is typed properly
      const { id } = req.params // { id: number }
      const { key } = req.query // { key: string }
      const { name } = req.body // { name: string }
      
      return { ...response }
    },
  }),
);

Validations can be defined, as shown above, with plain objects, but also you can define a different type:

app.post('/path/:id',
  handler({
    // body can be of any type
    body: z.boolean().array().optional(),
    async handler(req) {
      // ...
    },
  }),
);

Validating response

Set result validation to make it type-safe and validated:

app.get('/path',
  handler({
    result: {
      name: z.string(),
    },
    async handler(req) {
      // TS error:
      // return { invalid: true }
      
      return { name: 'string' }
    },
  }),
);

To not mistakenly leak a password, and to not send extra data when it's not intended, result validation prevents it with a TS error:

app.get('/path',
  handler({
    result: {
      name: z.string(),
    },
    // TS error
    async handler(req) {
      return { name: 'string', password: '1234' };
    },
  }),
);

res.send is also performing safely:

app.get('/path',
  handler({
    result: {
      name: z.string(),
    },
    async handler(req, res) {
      // TS error
      res.send({ name: 'string', password: '1234' });
      
      // no error
      res.send({ name: 'string' });
    },
  }),
);

This library runs validations on responses by default when NODE_ENV !== 'production'.

You can override it by passing a boolean into checkResult:

import { makeHandler } from 'express-ts-handler';

export const handler = makeHandler<ZodSchema>({
  // never check:
  checkResult: false,
});

Special middlewares

We can define a regular function and call it inside a route handler, it's simpler than a middleware and is well typed:

// simple function
const authorizeUser = (req: Request) => {
  const token = req.header('Authorization');
  if (token === 'correct token') {
    return loadUserByToken(token);
  }
  throw new Error('Unauthorized');
};

app.get('/path', async (req) => {
  // no problems with types
  const user = await authorizeUser(req);
})

That works well, but for the case if we want first to run a middleware, and only after it is passed to run validations, this library supports the following:

const authorizeUser = async (req: Request) => {
  const token = req.header('Authorization');
  if (token === 'correct token') {
    const user = loadUserByToken(token);
    // assign user object to req, return result
    return Object.assign(req, { user });
  }
  throw new Error('Unauthorized');
};

app.get('/path',
  handler({
    use: authorizeUser,
    async handler(req, res) {
      // req.user has a correct type
      req.user.id
    },
  }),
);

use can take a single function, or an array of multiple middlewares:

// may be sync
const one = (req: Request) => {
  return Object.assign(req, { one: 123 });
};

// may be async
const two = async (req: Request) => {
  return Object.assign(req, { two: 'string' });
};

app.get('/path',
  handler({
    use: [one, two],
    async handler(req, res) {
      // req.one is string
      req.one.toLowerCase()
      // req.one is a number
      req.two.toFixed(2)
    },
  }),
);

Middlewares accept the same parameters as the Express ones, no problems with async errors, they can call next function.

const one = async () => {
  throw new Error('error from middleware')
};

// calling next(err) is equivalent to throwing
const two = async (req, res, next) => {
  next(new Error('error from middleware'))
};

app.get('/path',
  handler({
    use: [one, two],
    async handler(req, res) {
      // ...
    },
  }),
);