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

@zhttp/core

v1.1.3

Published

A minimal, strongly typed HTTP library with Zod validation

Downloads

165

Readme

zhttp, a minimal, typesafe HTTP library with Zod validation

zhttp is a minimal, typesafe, OpenAPI compatible HTTP library. It's build around express and Zod.

It solves some of the major pains of building an API with express (handler typing, error handling, input/output validation, openapi...) while attempting to stay as flexible (read: as close to plain express) as possible.

🧪 Try out zhttp on Stackblitz!

Installation

npm install @zhttp/core @zhttp/errors zod

Basic usage examples

// ./examples/basic-usage.ts

import { z } from 'zod'
import {
  Server,
  controller,
  get,
  extendZodWithOpenApi,
  zApiOutput,
  apiResponse,
  openapiController
} from '@zhttp/core'

extendZodWithOpenApi(z)
// ⬆ What this allows you to do is to optionally add OAS info
// to a Zod validation schema using zodSchema.openapi(...)
// If this Zod schema is used in the input or output of an endpoint,
// the info provided will be included in the generated openapi spec.
//
// Exmaple:

const zHelloResponse = zApiOutput(z.object({
  greeting: z.string().openapi({ example: 'Hello Joske!' })
})).openapi('HelloResponse')

const helloController = controller('Hello')
  .description('This controller says hello to everyone')

helloController.endpoint(
  get('/hello')
    .input({
      params: z.object({
        name: z.string().optional()
      })
    })
    .response(zHelloResponse)
    .handler(async (input) => {
      return apiResponse({
        // Both the input object ⬇ and the handler response are strongly typed :)
        greeting: `Hello ${input.params.name ?? 'everybody'}!`
      })
    })
)

const server = new Server({
  controllers: [
    helloController,
    openapiController
  ],
  middlewares: []
}, {
  port: 3000,
  oasInfo: {
    title: 'A very cool api',
    version: '1.0.0'
  }
})

// eslint-disable-next-line @typescript-eslint/no-floating-promises
server.start()

Concepts

Endpoints

Endpoints are the building blocks of your API. They define the HTTP methods, paths, inputs, outputs, and behavior for each API call. As can be seen in the example below, endpoints are defined using a function chaining approach. These functions are further refered to as 'modulators'.

Handlers and input/output typing

Endpoint handlers are similar to plain express handlers, with some extra feastures for strict input and output typing. Rather than the typical 2 express handler parameters (request and response), there's three: inputs, request, response. inputs is a typed object containing everything defined in the input schema. request and response are simply the express request and response objects.

Sending out a response is as simle as returning a value in the handler function. Handler responses, just like inputs, are strictly typed. When an output schema is specified and the handler function doesn't return a value that corresponds to said schema, typescript will comlain.

By default, every endpoint will send its response with a application/json content type. This is typial for APIs, but there might be exceptions. You can override this content type using the responseContentType modulator.

Using req/res directly

You can take input via the request object and use the response object to send a response in typical express manner, so migrating from express should be fairly trivial. However, beware that where you do this, you'll lose:

  • strict typing
  • output validation
  • AFTER-type middlewares

Routing

An important distinction between plain express and zhttp is that zhttp – consciously – doesn't really support routing. The paths you see in your endpoint definitions are the paths that can be called; No hidden prefixes.

Basic endpoint example

// ./examples/concept-endpoint.ts

import { z } from 'zod'
import { endpoint, get } from '@zhttp/core'

const zGreetingOutput = z.object({
  message: z.string()
})

const zGreetingInput = {
  query: z.object({
    name: z.string().optional()
  })
}

// ⬇ For common http methods (get, post, put, del), utility functions are available:
get('/hello', 'getGreeting')
  .description('Say hello to everyone')
  .input(zGreetingInput)
  .response(zGreetingOutput)
  .handler(async ({ query }) => {
    return {
      message: `Hello ${query.name ?? 'everyone'}!`
    }
  })

// `endpoint` is a generic function which supports every http method.
endpoint('get', '/goodbye', 'getGoodbye')
  .description('Say goodbye to everyone')
  .input(zGreetingInput)
  .response(zGreetingOutput)
  .handler(async ({ query }) => {
    return {
      message: `Goodbye ${query.name ?? 'everyone'}!`
    }
  })

Controllers

An controller, essentially, is nothing but a group of endpoints. Just like individual endpoints, controllers can be assigned middlewares. Controllers do not serve as routers. Every endpoint path should be a complete path.

Basic controller example

// ./examples/concept-controller.ts

import { z } from 'zod'
import { controller, get } from '@zhttp/core'

export const greetingController = controller('greeting')
  .description('A controller that greets the world.')

greetingController.endpoint(
  get('/hello', 'getGreeting')
    .description('Say hello to everyone')
    .input({
      query: z.object({
        name: z.string().optional()
      })
    })
    .response(z.object({
      message: z.string()
    }))
    .handler(async ({ query }) => {
      return {
        message: `Hello ${query.name ?? 'everyone'}!`
      }
    })
)

Middleware

A middleware is a function that operates between an incoming request and the corresponding outgoing response. It serves as a processing layer before or after an endpoint handler, carrying out tasks like logging, authentication, and other sorts of data manipulation.

Middlewares in zhttp are essentially just express middlewares, with two extra properties: their type (indicating when to run them), and an optional name. Middlewares can be bound on multiple levels:

  • The server
  • A controller
  • An endpoint

Basic middleware example

// ./examples/concept-middleware.ts

import { type Request, type Response, type NextFunction } from 'express'
import { middleware, MiddlewareTypes } from '@zhttp/core'

export const lastVisitMiddleware = middleware({
  name: 'lastVisitMiddleware',
  type: MiddlewareTypes.BEFORE,
  handler (req: Request, res: Response, next: NextFunction) {
    const now = new Date()
    const lastVisitCookieValue = req.cookies.beenHereBefore
    const lastVisitTime = lastVisitCookieValue != null ? new Date(String(lastVisitCookieValue)) : undefined
    res.cookie('beenHereBefore', now.toISOString())
    if (lastVisitTime == null) {
      console.log('Seems like we\'ve got a new user 👀')
      next(); return
    }
    const daysSinceLastVisit = (now.getTime() - lastVisitTime.getTime()) / (1000 * 60 * 60 * 24)
    console.log(`It's been ${daysSinceLastVisit} days since this user last visited.`)
    next()
  }
})

Server

Basic server example

// ./examples/concept-server.ts

import { Server } from '@zhttp/core'
import { greetingController } from './concept-controller.js'
import { lastVisitMiddleware } from './concept-middleware.js'

export const server = new Server({
  controllers: [greetingController],
  middlewares: [lastVisitMiddleware]
}, {
  port: 8080
})

// eslint-disable-next-line @typescript-eslint/no-floating-promises
server.start()

OpenAPI

openapiController

The package exports a special controller openapiController. When used, this controller exposes routes /openapi.json (the OpenAPI json spec) and /api.html (a RapiDoc api interface).

Programmatic access

The openapi definition can be directly from the server object.

// ./examples/direct-openapi.ts

import { server } from './concept-server.js'

console.log(
  server.oasInstance.getJsonSpec()
)

Errors

zhttp has a built in error handler, which will catch any sort of error thrown in an endpoint or middleware.

@zhttp/errors

Any type of unknown error will be logged and will result in a InternalServerError response (http status code 500).

If you want to throw a specific type of error which will be reflectced in the http response, you can use the @zhttp/errors library.

// ./examples/concepts-errors.ts

import { z } from 'zod'
import { controller, get } from '@zhttp/core'
import { NotFoundError } from '@zhttp/errors'

// Let's presume we're talking to some sort of database
const db: any = undefined

export const vegetablesController = controller('vegetables')

vegetablesController.endpoint(
  get('/vegetables/:vegetableId', 'getVegetableDetails')
    .input({
      params: z.object({
        vegetableId: z.string().uuid()
      })
    })
    .response(z.object({
      message: z.string()
    }))
    .handler(async ({ params: { vegetableId } }) => {
      const vegetableDetails = await db.getVegetableById(vegetableId)
      if (vegetableDetails == null) {
        // ✨✨✨✨✨✨✨✨✨
        throw new NotFoundError(`Vegetable with id ${vegetableId} does not exist`)
        // ⬆ This will result in a 404 response
        // ✨✨✨✨✨✨✨✨✨
      }
      return vegetableDetails
    })
)

Validation errors

If an error is detected as part of the request input validation, the server will send a ValidationError response, including an error message explaining what's wrong.

If an error is detected as part of the request output validation, an InternalServerError is returned, and error message is logged.

// ./examples/validation-errors.ts

import { z } from 'zod'
import { controller, get } from '@zhttp/core'

export const validationExampleController = controller('validationExample')

validationExampleController.endpoint(
  get('/hello', 'getGreeting')
    .input({
      query: z.object({
        // If a name shorter than 5 characcters is provided, then the server will responde with a ValidationError.
        name: z.string().min(5)
      })
    })
    .response(z.object({
      message: z.string()
    }))
    .handler(async ({ query }) => {
      return {
        message: `Hello ${query.name ?? 'everyone'}!`
      }
    })
)

validationExampleController.endpoint(
  get('/goodbye', 'getGoodbye')
    .input({
      query: z.object({
        name: z.string().optional()
      })
    })
    .response(z.object({
      message: z.string()
    }))
    .handler(async ({ query }) => {
      return {
        thisKeyShouldntBeHere: 'noBueno'
      } as any
      // ⬆ As zhttp is typesafe, you actually have to manually $x&! up the typing
      // to provoke an output validation error :)
      // This will result in an InternalServerError.
    })
)

Order of execution

  • Server 'BEFORE' middlewares
  • Controller 'BEFORE' middlewares
  • Endpoint 'BEFORE' middlewares
  • Endpoint handler
  • Endpoint 'AFTER' middlewares
  • Controller 'AFTER' middlewares
  • Server 'AFTER' middlewares

CommonJS support

📰 CommonJS is hurting JavaScript

The JavaScript ecosystem is (slowly but steadily) moving towards ESM and away from CommonJS. zhttp is build as an ESM module. It's strongly encouraged to use it like that.

CommonJS is currently supported; the packages include both builds for ESM and CommonJS. You can use zhttp both ways.

If major issues with supporting CommonJS were to come up, or if we'd notice that the package would become too big (by essentially having to ship the build code twice), CommonJS support might be dropped in the future.