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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@safekit/route

v0.2.1

Published

A type-safe url builder with full type safety and runtime validation

Readme

@safekit/route

A type-safe URL builder with validation support for TypeScript applications. Build URLs with path parameters and query strings while maintaining full type safety and runtime validation.

npm version TypeScript License: MIT

Features

  • Type-safe - Full TypeScript support with path and query parameter inference
  • Validation - Support for Standard Schema compatible validation libraries (Zod, Valibot, ArkType, etc.)
  • Flexible - Multiple query string serialization formats
  • Lightweight - Only depends on qs for query serialization
  • Framework agnostic - Works with any TypeScript project

Installation

npm install @safekit/route
yarn add @safekit/route
bun add @safekit/route

Quick Start

Commonly used to have a registry of your frontend routes for navigating to different pages via links or buttons

import { z } from "zod";
import { createRouter } from "@safekit/route";

// Create a router
const router = createRouter({
  baseUrl: "http://localhost:3000",
});

// Register routes
const appRoutes = router.register([
  { path: "/login" },
  { path: "/register", query: { email: z.string().email() } },
  { path: "/users/:userId/profile", params: { userId: z.string() } },
] as const);

// Generate paths and URLs
const homePath = appRoutes.path({ path: "/login" });
// Result: "/login"

const userPath = appRoutes.path({
  path: "/users/:userId/profile",
  params: { userId: "123" },
});
// Result: "/users/123/profile"

const searchUrl = appRoutes.href({
  path: "/register",
  query: { email: "[email protected]" },
});
// Result: "http://localhost:3000/register?email=john.smith%40gmail.com"

You can also use it to have a registry of your backend urls

// Create a router
const router = createRouter({
  baseUrl: "https://app.example.com/api",
});

// Register routes
const apiRoutes = router.register([
  {
    path: "/auth/social/callback/:provider",
    params: z.object({
      provider: z.enum(["google", "github"]),
    }),
    query: z.object({
      code: z.string(),
      state: z.string().optional(),
      error: z.string().optional(),
      error_description: z.string().optional(),
    }),
  },
] as const);

// Generate callback URLs
const googleCallback = apiRoutes.href({
  path: "/auth/social/callback/:provider",
  params: { provider: "google" },
  query: { code: "auth_code_123", state: "random_state_token" },
});
// Result: "https://app.example.com/api/auth/social/callback/google?code=auth_code_123&state=random_state_token"

Type-Safe API

@safekit/route leverages TypeScript to provide a fully type-safe experience from registration to usage.

Path Type Inference

By using as const on your route definitions, the router can infer a union type of all valid path strings. This type can be extracted using typeof router.Paths.

const appRoutes = router.register([
  { path: "/home" },
  { path: "/users/:userId" },
] as const); // 'as const' is essential!

// Extract the type
export type AppPath = typeof appRoutes.Paths;
// AppPath is now "/home" | "/users/:userId"

// Use it for type-safe functions
function navigate(path: AppPath) { /* ... */ }

navigate("/home"); // ✅ OK

// ❌ TypeScript Error! The literal string "/users/123" is not assignable
// to the type '"/home" | "/users/:userId"'.
navigate("/users/123");

// ✅ The correct way is to build the path, which is also type-safe:
const userPath = appRoutes.path({ path: "/users/:userId", params: { userId: "123" } });
// navigate(userPath) would work if the function accepted a string

Parameter and Query Inference

When you use .path(), .URL(), or .href(), the types for params and query are automatically inferred based on the path you provide and the schemas you registered.

const routes = router.register([
  { path: "/users/:userId", params: z.object({ userId: z.string() }) },
  { path: "/search", query: z.object({ q: z.string() }) },
] as const);

// TypeScript knows 'params' is required and must have 'userId: string'
routes.path({
  path: "/users/:userId",
  params: { userId: "abc" },
});

// TypeScript knows 'query' is required and must have 'q: string'
routes.href({
  path: "/search",
  query: { q: "hello" },
});

// TypeScript will throw an error if you use the wrong params
routes.path({
  path: "/search",
  // @ts-expect-error: 'params' is not applicable to '/search'
  params: { id: 123 },
});

API Reference

createRouter(options?)

Creates a new router instance.

Options:

  • baseUrl?: string - Default base URL for absolute URLs
  • querySerializer?: QuerySerializer - Custom query string serializer

router.register(definitions)

Registers route definitions with the router.

Route Definition:

interface RouteDefinition {
  path: string; // URL pattern with :param syntax
  // Standard Schema compatible objects that produce object-like outputs
  params?: StandardSchemaV1<unknown, Record<string, unknown>>;
  query?: StandardSchemaV1<unknown, Record<string, unknown>>;
}

Note: To use params or query parameters, you must register validation schemas during route registration for type safety. Schemas must produce object-like outputs (e.g., Record<string, unknown>) to work with the router's path parameter and query serialization features.

Methods

router.path(options)

Returns a relative path string.

// Route must have schemas registered to use params/query
const router = createRouter().register([
  {
    path: "/users/:id",
    params: z.object({ id: z.string() }),
    query: z.object({ tab: z.string() }),
  },
] as const);

const path = router.path({
  path: "/users/:id",
  params: { id: "123" },
  query: { tab: "settings" },
});
// Result: "/users/123?tab=settings"

router.URL(options)

Returns a URL object.

// Route must have query schema registered to use query
const router = createRouter().register([
  {
    path: "/search",
    query: z.object({ q: z.string() }),
  },
] as const);

const url = router.URL({
  path: "/search",
  query: { q: "test" },
});
//  Result: URL object where
//  url.href === "https://api.example.com/search?q=test"
//  origin: 'https://api.example.com',
//  protocol: 'https:',
//  host: 'api.example.com',
//  hostname: 'api.example.com',
//  pathname: '/search',
//  search: '?q=test',
//  searchParams: URLSearchParams { 'q' => 'test' },

router.href(options)

Returns an absolute URL string.

const href = router.href({
  path: "/search",
  query: { q: "test" },
  baseUrl: "https://staging.example.com", // Optional override
});
// Result: "https://staging.example.com/search?q=test"

router.paths()

Returns array of registered path patterns.

const paths = router.paths();
// Result: ["/home", "/users/:userId/profile", "/search"]

Validation Support

The package supports any validation library that follows the Standard Schema specification:

  • Zod (v3 and v4) - Use schemas directly or with .standardSchema() method
  • Valibot - Use schemas directly with Standard Schema support
  • ArkType - Use schemas directly with Standard Schema support
  • Effect Schema - Use with Schema.standardSchemaV1() wrapper
  • Any other Standard Schema v1 compatible library

All validation libraries must implement the Standard Schema interface with a "~standard" property containing the validation logic.

Nested Objects and Complex Query Parameters

The default qs serialization handles deeply nested objects and arrays, making it perfect for complex API endpoints like paginated queries:

import { createRouter } from "@safekit/route";
import { z } from "zod";

// Setup router with query validation
const router = createRouter().register([
  {
    path: "/users",
    query: z.object({
      filter: z.object({
        status: z.string(),
        age: z.number(),
        userIds: z.array(z.string()),
      }),
      orderBy: z.array(z.object({
        field: z.string(),
        direction: z.string(),
      })),
      page: z.number(),
      limit: z.number(),
    }),
  },
] as const);

// Example: Paginated endpoint with complex filtering
const queryObject = {
  filter: {
    status: "active",
    age: 21,
    userIds: ["abc1", "xyz2"],
  },
  orderBy: [
    { field: "lastName", direction: "asc" },
    { field: "createdAt", direction: "desc" },
  ],
  page: 20,
  limit: 10,
};

const url = router.href({
  path: "/users",
  query: queryObject,
});

// Default (brackets) serialization output:
https://api.example.com/users?
  filter[status]=active&
  filter[age]=21&
  filter[userIds][]=abc1&
  filter[userIds][]=xyz2&
  orderBy[][field]=lastName&
  orderBy[][direction]=asc&
  orderBy[][field]=createdAt&
  orderBy[][direction]=desc&
  page=20&
  limit=10

Query String Serialization

Choose from multiple query string formats:

import { createRouter, querySerializers } from "@safekit/route";

createRouter({ querySerializer: querySerializers.brackets });
createRouter({ querySerializer: querySerializers.comma });
createRouter({ querySerializer: querySerializers.native });
createRouter({ querySerializer: querySerializers.indices });

Arrays:

const query = { tags: ["a", "b"] };
// Brackets (default): tags[]=a&tags[]=b
// Comma:              tags=a,b
// Native:             tags=a&tags=b
// Indices:            tags[0]=a&tags[1]=b

Objects:

const query = { filter: { status: "active", type: "user" } };
// Brackets (default): filter[status]=active&filter[type]=user
// Comma:              filter[status]=active&filter[type]=user
// Native:             filter={"status":"active","type":"user"} (Not recommended for objects)
// Indices:            filter[status]=active&filter[type]=user

Contributing

See CONTRIBUTING.md for contribution guidelines.

License

MIT © safekit