@safekit/route
v0.2.1
Published
A type-safe url builder with full type safety and runtime validation
Maintainers
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.
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
qsfor query serialization - ✅ Framework agnostic - Works with any TypeScript project
Installation
npm install @safekit/route
yarn add @safekit/route
bun add @safekit/routeQuick 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 stringParameter 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 URLsquerySerializer?: 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=10Query 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]=bObjects:
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]=userContributing
See CONTRIBUTING.md for contribution guidelines.
License
MIT © safekit
