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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tetsujs/typebox

v0.5.3

Published

TypeBox schemas as Tetsu DTOs: compiled validation, type inference and OpenAPI

Readme

@tetsujs/typebox

TypeBox schemas as DTOs: compiled validation, full type inference, and the same schema in the OpenAPI document.

bun add typebox @tetsujs/typebox

Usage

Wrap a TypeBox schema with tb() where the DTO is declared, and use it in any part of a route's schema:

import { tb, Type } from "@tetsujs/typebox";

export const CreateUser = tb(
  Type.Object({
    name: Type.String({ minLength: 1 }),
    email: Type.String({ format: "email" }),
  }),
);

export const UserParams = tb(Type.Object({ id: Type.Integer() }), { convert: true });

update = route({
  method: "PUT",
  path: "/users/:id",
  schema: { params: UserParams, body: CreateUser },
  handler: (ctx) => this.users.update(ctx.params.id, ctx.body),
  //                                   ^? number     ^? { name: string; email: string }
});

The schema is compiled once, when tb() runs, and every request uses the compiled check. Type is TypeBox's own, re-exported, so TypeBox's documentation applies as it is; typebox stays a peer dependency, so the application picks its version.

Options

| Option | Effect | Use for | | --- | --- | --- | | convert | converts before checking: "42" → 42 | params, query, headers, which arrive as strings | | clean | drops properties the schema does not declare | response DTOs, so nothing undeclared leaks | | defaults | fills in a declared default when a value is missing | queries with optional parameters, configuration | | issues | "detailed" (default) or "summary" | "summary" gives one issue per failed value — much cheaper on large bodies | | vendor | the vendor name reported to the core | custom tooling |

All are off by default, and none of them modifies the value it was given. Wrapping a DTO again replaces its options rather than adding to them: tb(CreateOrder, { convert: true, issues: "summary" }) keeps convert only because it says so.

A property with a default and defaults: true is documented as optional on input, since the client does not have to send it.

Files

file() and files() validate uploads in a bodyType: "form" body:

import { file, files, tb, Type } from "@tetsujs/typebox";

const Upload = tb(
  Type.Object({
    title: Type.String({ minLength: 1 }),
    avatar: file({ maxSize: "5m", type: "image" }),
    gallery: files({ maxSize: "1m" }),
  }),
);

route({
  method: "POST",
  path: "/uploads",
  bodyType: "form",
  schema: { body: Upload },
  handler: (ctx) => store(ctx.body.title, ctx.body.avatar, ctx.body.gallery),
  //                                      ^? File          ^? File[]
});

| Option | Accepts | Checks | | --- | --- | --- | | maxSize | 5242880, "512k", "5m" | the largest file size | | minSize | the same | the smallest — 1 rejects the empty part an untouched input sends | | type | "image", "image/png", ["image", "application/pdf"] | the MIME type; "image" matches every image type |

files() always gives an array, even for a single file. These checks run after the body was read; the limit on what is read at all is maxBodySize, and it counts the multipart framing too, which is larger than it looks.

Error messages

Set your own message on a schema with errorMessage — one string for any failure, or one per keyword:

const CreateUser = tb(
  Type.Object({
    email: Type.String({
      format: "email",
      errorMessage: { format: "Not an email address", required: "Email is required" },
    }),
    password: Type.String({ minLength: 8, errorMessage: "At least 8 characters" }),
  }),
);

errorMessage is not included in the JSON Schema or the OpenAPI document. For several languages, keep schemas without messages and translate in an onError hook, keyed by each issue's path.

Every issue points at the field itself — a missing password is reported at ["body", "password"], not at body — and a union of literals fails with one issue listing the allowed values.

Codecs

A Type.Codec is validated as it arrives and handed over decoded:

const Instant = Type.Codec(Type.String({ format: "date-time" }))
  .Decode((value) => new Date(value))
  .Encode((value: Date) => value.toISOString());

const Stored = tb(Type.Object({ code: Type.String(), expiresAt: Instant }));

parse(Stored, await redis.hgetall(key)); // { code: string; expiresAt: Date }

Validating outside a request

parse() validates any value and returns it, or throws a ValidationError with every issue. It is synchronous, so it works at module level — for example, for the environment:

import { parse, tb, Type } from "@tetsujs/typebox";

const Env = tb(
  Type.Object({
    PORT: Type.Integer({ minimum: 1, maximum: 65_535, default: 3000 }),
    DATABASE_URL: Type.String({ format: "uri" }),
  }),
  { convert: true, defaults: true, clean: true },
);

export const env = parse(Env, Bun.env);

Inside a handler, a thrown ValidationError becomes the same 422 a rejected request gets.

Performance

TypeBox compiles each schema into a checking function, so valid bodies are checked several times faster than with other Standard Schema libraries — the bigger the body, the bigger the gain. From bun run --cwd bench validators, in nanoseconds per check:

| | Zod 4.6 | ArkType 2.2 | Valibot 1.5 | TypeBox via tb() | | --- | --- | --- | --- | --- | | small body, valid | 23 | 24 | 21 | 6.7 | | 20-item body, valid | 922 | 168 | 784 | 52 | | 20-item body, one item invalid | 1,020 | 3,260 | 936 | 21,090 | | memory to import | +21 MB | +57 MB | +3 MB | +36 MB |

Describing a failure in detail is TypeBox's slow path; issues: "summary" answers the invalid body above in 76 ns. TypeBox fits large bodies, mostly valid traffic and schemas that double as documentation; a lighter library fits when memory and startup matter more.

OpenAPI 3.1 and JSON Schema 2020-12 are emitted as they are; older dialects throw rather than being converted approximately.