A type-safe URL builder library based on your route definitions
- 🔒 Type-safe route definition and path construction
- 📦 Lightweight and no dependencies
- 🥰 Smooth development experience with type inference and autocompletion
- 👍 Supports use cases like Base URL and Catch-all Parameters
- 🧩 Strict URL type inference using template literal types
npm install routopiaimport { routes, empty, type } from 'routopia';
export const myRoutes = routes({
"/users": {
get: empty,
post: empty,
},
"/path/[id]": {
get: {
params: {
id: type as number,
},
queries: {
q: type as string | undefined,
},
},
},
});import { myRoutes } from './path/to/myRoutes';
myRoutes["/users"].get();
myRoutes["/users"].post();
// => "/users"
myRoutes["/path/[id]"].get({ params: { id: 123 } });
// => "/path/123"
myRoutes["/path/[id]"].get({ params: { id: 123 }, queries: { q: "query" } });
// => "/path/123?q=query"Tip
The return value is inferred in detail by template literal types.
For example, if const path = myRoutes["/users"].get(), the type of path will be "/users".
If you want to receive it as a string type, please add a type annotation:
const path: string = myRoutes["/users"].get()
Provides type-safe route definitions including path parameters and query parameters, powered by strong type inference and IDE autocompletion features.
The main differences from other libraries are as follows:
- Autocomplete works during definition.
- Autocomplete guides you to the correct path during usage.
- Detailed inference is obtained through template literal types.
routopia focuses on declaratively and simply getting type-safe URLs.
If you need more advanced features like automatic generation or regular expressions, other libraries might be better.
Conversely, routopia might be a good match for the following cases:
- When you need to handle links to external sites or third-party API endpoints without an available SDK
- Managing URLs with custom schemas (e.g.,
myapp://path/to/resource) - When you're not using ecosystems like OpenAPI generators for some reason
- When you want to simply use features like Next.js Route Handlers
- When managing simple internal links or using typedRoutes in Next.js
- When you need a simple URL builder
- No Parameters
- Path Parameters
- Catch-all Parameters
- Query Parameters
- Hash
- Base URL
- Shorthand
- Best Practices
- Specify
emptyif no parameters are needed.
import { routes, empty } from 'routopia';
const myRoutes = routes({
"/path": {
get: empty,
post: empty,
put: empty,
delete: empty,
},
});
myRoutes["/path"].get();
myRoutes["/path"].post();
myRoutes["/path"].put();
myRoutes["/path"].delete();
// => All resolve to "/path"- Enclose with
[]like[param]. - Multiple path parameters can also be specified.
- Path parameters are defined within the
paramsobject. - Specify the
typeof path parameters using type assertion (type as {Type}or<{Type}>type) with the dedicated type object. - The type of path parameters can be specified satisfying
string | number.
import { routes, type } from 'routopia';
const myRoutes = routes({
"/path/[id]": {
get: {
params: {
id: type as number,
// <number>type is also OK
},
},
},
"/path/[param1]/[param2]": {
get: {
params: {
param1: type as string,
param2: type as string | number,
},
},
},
});
myRoutes["/path/[id]"].get({ params: { id: 123 } });
// => "/path/123"
myRoutes["/path/[id]"].get({ params: { id: "abc" } });
// ^^^^^
// Error: Type 'string' is not assignable to type 'number'
myRoutes["/path/[param1]/[param2]"].get({
params: { param1: "abc", param2: 123 },
});
// => "/path/abc/123"
myRoutes["/path/[id]"].get();
// ^^^^
// Error: Path parameters cannot be omitted when called.- Define catch-all parameters like
[...param]. - The type of catch-all parameters can be specified satisfying
(string | number)[]. - Using double brackets like
[[...param]]allowsundefinedin addition to the above. - This feature is equivalent to Next.js's Catch-all Segments.
import { routes, type } from 'routopia';
const myRoutes = routes({
"/path/[...slug]": {
get: {
params: {
slug: type as string[],
},
},
},
"/path/[[...slug]]": {
get: {
params: {
slug: type as number[],
},
},
},
});
myRoutes["/path/[...slug]"].get({
params: { slug: ["abc", "def"] },
});
// => "/path/abc/def"
myRoutes["/path/[...slug]"].get();
// ^^^^
// Error: Catch-all parameters cannot be omitted when called.
myRoutes["/path/[[...slug]]"].get({
params: { slug: [123, 456] },
});
// => "/path/123/456"
myRoutes["/path/[[...slug]]"].get();
// => "/path"
// Optional Catch-all parameters can be omitted when called.- Query parameters are defined within the
queriesobject. - Types other than
objectcan be specified for query parameters. - Including
undefinedmakes them optional (omittable).
import { routes, type } from 'routopia';
const myRoutes = routes({
"/required": {
get: {
queries: {
str: type as string,
num: type as number,
bool: type as boolean,
arr: type as string[],
opt: type as string | undefined,
},
},
},
"/optional": {
get: {
queries: {
str: type as string | undefined,
num: type as number | undefined,
bool: type as boolean | undefined,
arr: type as string[] | undefined,
},
},
},
});
myRoutes["/required"].get({
queries: {
str: "abc",
num: 123,
bool: true,
arr: ["a", "b", "c"]
},
});
// => "/required?arr=a&arr=b&arr=c&bool=true&num=123&str=abc"
myRoutes["/required"].get();
// ^^^
// Error: Cannot be omitted if there are required query parameters.
myRoutes["/optional"].get();
// => "/optional"Tip
The order of query parameters is sorted, making it compatible with caching mechanisms like SWR that use URLs as keys.
- Define the
hashkey if needed. - The
hashkey can be specified satisfying thestringtype. - Using a Union type is safer for actual use, but specifying
stringto accept anything is also possible. - The
hashkey is always omittable, even without includingundefined.
import { routes, type } from 'routopia';
const myRoutes = routes({
"/path": {
get: {
hash: type as "anchor1" | "anchor2",
},
},
"/any": {
get: {
hash: type as string,
},
},
});
myRoutes["/path"].get({ hash: "anchor1" });
// => "/path#anchor1"
myRoutes["/path"].get({ hash: "unknown" });
// ^^^^^
// Error: Type '"unknown"' is not assignable to type '"anchor1" | "anchor2"'.
myRoutes["/any"].get({ hash: "unknown" });
// => "/any#unknown"
myRoutes["/path"].get();
// => "/path"- The routes function can accept a string as the first argument to specify a Base URL.
- In that case, specify the schema definition as the second argument.
- Note that the Base URL is simply prefixed.
import { routes, empty } from 'routopia';
const myUsersRoutes = routes("/users", {
"/path": {
get: empty,
},
});
myUsersRoutes["/path"].get();
// => "/users/path"
const myApiRoutes = routes("https://api.example.com", {
"/path": {
get: empty,
},
});
myApiRoutes["/path"].get();
// => "https://api.example.com/path"- You can directly define parameters by omitting the HTTP method definition.
- In this case, it is equivalent to defining the GET method.
import { routes, empty, type } from 'routopia';
const myRoutes = routes({
"/short": empty,
// = "/short": { get: empty }
"/short/[param]": {
params: {
param: type as string,
},
queries: {
q: type as string | undefined,
},
hash: type as string,
},
// = "/short/[param]": { get: { params: {...}, queries: {...}, hash: ... } }
});
// Use it by calling the `get` method
myRoutes["/short"].get();
// => "/short"
myRoutes["/short/[param]"].get({
params: { param: "abc" },
queries: { q: "query" },
hash: "anchor",
});
// => "/short/abc?q=query#anchor"Warning
When using Shorthand, you cannot combine it with other HTTP method definitions within the same endpoint.
While it's possible to use them together across different endpoints, you should split definition files to prevent notation inconsistencies within the same domain.
Examples of Mixing HTTP method definitions and Shorthand
import { routes, empty, type } from 'routopia';
// ❌: Cannot combine Shorthand notation with other method definitions within the same endpoint
const error = routes({
"/short/mixed": {
queries: { q: type as string },
post: empty,
// ^^^^^^^^^^^
},
});
// ⚠️: Can be used together across different endpoints, but causes notation inconsistencies
const notGood = routes({
"/hoge": empty,
// :
"/foo": { get: empty },
});
// ✅: Unify notation between endpoints within the same file
const good = routes({
"/hoge": { get: empty },
// :
"/foo": { get: empty },
});
// ✅: Or split definition files and unify notation by domain
// hoge.ts
const hogeRoutes = routes({
"/hoge": empty,
});
// foo.ts
const fooRoutes = routes({
"/foo": { get: empty },
});- Create an abstraction layer by wrapping
routopia. - It's also possible to specify the Base URL collectively.
- Use
ExpectedSchemawith generics for the argument type when wrapping. - Split definition files by domain as needed to prevent them from becoming too large
// createMyApiRoutes.ts
import { routes, empty, type, ExpectedSchema } from 'routopia';
const API_BASE_URL = "https://api.example.com";
export function createMyApiRoutes<T extends ExpectedSchema<T>>(schema: T) {
return routes(API_BASE_URL, schema);
}
export const schema = { empty, type };// userRoutes.ts
import { createMyApiRoutes, schema } from './path/to/createMyApiRoutes';
export const userRoutes = createMyApiRoutes({
"/users": schema.empty,
"/users/[id]": {
params: {
id: schema.type as number,
},
},
});// postRoutes.ts
import { createMyApiRoutes, schema } from './path/to/createMyApiRoutes';
export const postRoutes = createMyApiRoutes({
"/posts": {
get: { queries: { q: schema.type as string | undefined } },
},
"/posts/[id]": {
get: { params: { id: schema.type as number } },
post: { params: { id: schema.type as number } },
put: { params: { id: schema.type as number } },
delete: { params: { id: schema.type as number } },
},
});// Example Usage
import { userRoutes } from './path/to/userRoutes';
import { postRoutes } from './path/to/postRoutes';
userRoutes["/users"].get();
// => "https://api.example.com/users"
postRoutes["/posts/[id]"].get({ params: { id: 123 } });
// => "https://api.example.com/posts/123"Warning
For convenience, it's possible to combine multiple route definitions as shown below, but be careful as TreeShaking will not work effectively
Example of TreeShaking not working effectively
// index.ts
import { userRoutes } from './path/to/userRoutes';
import { postRoutes } from './path/to/postRoutes';
export const apiRoutes = {
...userRoutes,
...postRoutes,
};
// Example Usage
import { apiRoutes } from './path/to/index';
apiRoutes["/users"].get();
apiRoutes["/posts/[id]"].get({ params: { id: 123 } });
// You can search all routes uniformly, but the bundle size will increase