seanpmaxwell/jet-paths

Type-safe TypeScript utility for composing, validating, and formatting URL paths from nested objects — with inferred path and search params.

TypeScript

11

120 commits

updated Sep 29, 2026

See the code

See what people are saying

SourceMessageScoreDate

Typesafe formatting API endpoints with jet-paths (r/typescript)

Built this for my React + ExpressJS project a few years ago to keep all of my endpoints in one place and reduce some of the boiler-plate for adding path/search parameters. Up until recently only path parameters (and not search parameters) were type-safe. I just did a major update so now search…

1

Sep 29, 2026

README

✈️   jet-paths

npm version npm downloads TypeScript bundle size License

Recursively formats an object of URLs so that full paths are set up automatically, allowing you to insert path parameters and append search parameters easily and consistently.

* * *

👀 At a glance

Installation:

npm install jet-paths

Snippet:

const Paths = jetPaths({
  $path: '/api',
  Users: {
    $path: '/users',
    Get: '/all',
    One: '/:id',
  },
});

Paths.Users();                 // '/api/users'
Paths.Users.$path;             // '/users'

Paths.Users.Get();             // '/api/users/all'
Paths.Users.Get.$path;         // '/all'

Paths.Users.One({ id: 5 });    // '/api/users/5'
Paths.Users.One({ name: 5 });  // ❌ Type error: 'name' does not exist in type '{ id: Primitive }'
Paths.Users.One();             // ❌ Type error: expected at least one argument
Paths.Users.One.$path;         // '/:id'
Paths.Users.One.$tmpl;         // '/api/users/:id'

Paths.$path;                   // '/api'

Adding a path parameter (e.g. /:id) creates a type safe object which must be passed as the first argument to the URL function.

* * *

🤔 Why jet-paths?

  • Type-safe, single source of truth for all your routes
  • Full URLs out of the box with built-in type-safe formatting functions
  • Path and search params are type-checked, validated at runtime, and URL-encoded
  • .$path and .$tmpl give you the local and full unformatted path templates
  • Small, lightweight, and zero dependency: 2.1 kB gzipped + minified

* * *

⚡ Tutorial

Another, more complete snippet

import jetPaths from 'jet-paths';

const Paths = jetPaths(
  {
    $path: '/api',
    Users: {
      $path: '/users',
      Get: '/all',
      Add: '/add',
      Update: '/update',
      Delete: '/delete/:id',
    },
    Posts: {
      $path: '/posts',
      Get: '/all?<q!><page>', // <- search parameters
      Add: '/add',
      Update: '/update',
      Delete: '/delete/:id', // <- path parameters
      Private: {
        $path: '/private',
        Get: '/all',
        Delete: '/delete/:foo/bar/:id', // <- multiple path parameters
      },
    },
  },
  { prepend: 'localhost:3000' },
);

The object above is formatted into type-safe routes:

Paths.Users();                                    // "localhost:3000/api/users"
Paths.Users.$path;                                // "/users"
Paths.Users.Get({ page: 2 });                     // "localhost:3000/api/users/all?page=2"
Paths.Users.Delete({ id: 1 });                    // "localhost:3000/api/users/delete/1"
Paths.Users.Delete.$tmpl;                         // "localhost:3000/api/users/delete/:id"
Paths.Posts.Get({ q: 'x', page: 2 });             // "localhost:3000/api/posts/all?q=x&page=2"
Paths.Posts.Get({ q: 'x', pg: 3 });               // ❌ type error, pg is not declared
Paths.Posts.Private.Delete({ foo: 'a', id: 2 });  // "localhost:3000/api/posts/private/delete/a/bar/2"
Paths.Posts.Private.Delete({ foo: 'a' });         // ❌ type error, id is missing

If you store the definition in a variable before passing it to jetPaths, use as const to preserve the literal templates. TypeScript rejects widened string templates because their parameter names cannot be inferred safely:

const definition = {
  $path: '/api',
  One: '/users/:id',
} as const;

const UserPaths = jetPaths(definition);
UserPaths.One({ id: 5 }); // "/api/users/5"

Insert path parameters

Insertion functions always require path parameters as the first argument. Pass an optional second object to append search parameters:

// Insert path parameters without appending search parameters.
Paths.Users.Delete({ id: 1 });
// "localhost:3000/api/users/delete/1"

// Insert the same path parameters and append search parameters.
Paths.Users.Delete({ id: 1 }, { permanent: true });
// "localhost:3000/api/users/delete/1?permanent=true"

// The same applies to nested routes with multiple path parameters.
Paths.Posts.Private.Delete({ foo: 'a', id: 2 });
// "localhost:3000/api/posts/private/delete/a/bar/2"

Paths.Posts.Private.Delete({ foo: 'a', id: 2 }, { tags: ['draft', 'old'] });
// "localhost:3000/api/posts/private/delete/a/bar/2?tags=draft&tags=old"

An insertion function's .$tmpl keeps the placeholders intact. Missing or unknown path parameters are TypeScript errors, and throw at runtime:

Paths.Users.Delete.$tmpl; // "localhost:3000/api/users/delete/:id"

// @ts-expect-error - the path parameters object is required
Paths.Users.Delete();

// @ts-expect-error - "id" is missing
Paths.Users.Delete({}, { permanent: true });

// @ts-expect-error - the parameter is named "id", not "userId"
Paths.Users.Delete({ userId: 1 });

Appending search parameters without declaring them

For routes without path parameters, pass search parameters as the first argument, or no arguments when you don't need a query string. Arrays become repeated keys, undefined values are omitted, and false and 0 are preserved:

Paths.Users.Get();
// "localhost:3000/api/users/all"

Paths.Users.Get({
  tags: ['admin', 'editor'],
  page: 0,
  active: false,
  q: undefined,
});
// "localhost:3000/api/users/all?tags=admin&tags=editor&page=0&active=false"

// Group routes can append search parameters too.
Paths.Users({ page: 2 });
// "localhost:3000/api/users?page=2"

Pass unencoded values: path and search parameters are encoded automatically. Search parameter values must be primitives or arrays of primitives; convert dates to strings first.

Paths.Users.Delete({ id: 'team/a' }, { reason: 'duplicate entry' });
// "localhost:3000/api/users/delete/team%2Fa?reason=duplicate%20entry"

Paths.Users.Get({ since: new Date('2026-01-01T00:00:00Z').toISOString() });
// "localhost:3000/api/users/all?since=2026-01-01T00%3A00%3A00.000Z"

Declaring search parameters

To type-check a route's search parameters, list their keys after a ? in the route, each in angle brackets. Keys are optional unless they end with ! (i.e. <q!>). Only declared keys are accepted, and required keys must have a value:

const UserPaths = jetPaths({
  $path: '/api',
  Users: {
    $path: '/users',
    Search: '/search?<q!><page><sort>',
    One: '/:id?<expand>',
  },
});

UserPaths.Users.Search({ q: 'sean', page: 2 });
// "/api/users/search?q=sean&page=2"

UserPaths.Users.One({ id: 5 }, { expand: true });
// "/api/users/5?expand=true"

// A required key means the search params are required too, so the path
// template is on "$tmpl". The declared keys aren't part of it.
UserPaths.Users.Search.$tmpl; // "/api/users/search"

// @ts-expect-error - the search params are required
UserPaths.Users.Search();

// @ts-expect-error - "q" is required
UserPaths.Users.Search({ page: 2 });

// @ts-expect-error - "pgae" isn't declared
UserPaths.Users.Search({ q: 'sean', pgae: 2 });

Routes without declared keys accept any search parameters. Undeclared and missing keys also throw at runtime, for JavaScript callers and values hidden behind broader types.

To split a long declaration across lines, use a template literal with a \ at the end of each line; it joins the lines without adding a newline. Don't join strings with +: TypeScript types the result as string, which jetPaths rejects.

const FlightPaths = jetPaths({
  $path: '/api',
  Search: `/flights?<from!><to!><depart!><adults!>\
<return><children><infants><cabin><stops><airline><currency><sort>`,
});

FlightPaths.Search({ from: 'LHR', to: 'JFK', depart: '2026-10-01', adults: 2 });
// "/api/flights?from=LHR&to=JFK&depart=2026-10-01&adults=2"

Inherit path parameters from parents

A parameter in a parent's $path is required by that group and all its children, even when a child's local path has no placeholders:

const OrgPaths = jetPaths({
  $path: '/api',
  Org: {
    $path: '/orgs/:orgId',
    Members: '/members',
    Member: '/members/:memberId',
  },
});

OrgPaths.Org({ orgId: 7 });
// "/api/orgs/7"

OrgPaths.Org.Members({ orgId: 7 }, { page: 2 });
// "/api/orgs/7/members?page=2"

OrgPaths.Org.Member({ orgId: 7, memberId: 42 });
// "/api/orgs/7/members/42"

OrgPaths.Org.Members.$path; // "/members"
OrgPaths.Org.Members.$tmpl; // "/api/orgs/:orgId/members"

Group routes without adding a path segment

Use $path: '' when a group should organize your code without changing the URL:

const PublicPaths = jetPaths({
  $path: '/api',
  Public: {
    $path: '',
    Health: '/health',
    Status: '/status',
  },
});

PublicPaths.Public.Health(); // "/api/health"
PublicPaths.Public.Status(); // "/api/status"

Destructure routes and paths

Destructuring is safe: route functions don't rely on this, so they work the same after being pulled off their parent, and a destructured $path or $tmpl keeps its exact type:

const { Get, Delete } = Paths.Users;
Get(); // "localhost:3000/api/users/all"
Get({ page: 2 }); // "localhost:3000/api/users/all?page=2"
Delete({ id: 1 }); // "localhost:3000/api/users/delete/1"

const { $path } = Paths.Users.Get; // "/all"

Object rest and spread ({ ...Paths.Users }) only copy a function's properties, so the result is a plain object, not a callable route.


Options: prepend and disableRegex

  • prepend (string | undefined, default undefined) — Prepends a string verbatim to every generated URL and complete path template. This prefix is not validated or interpolated: put /:name parameters in the route definitions, not in prepend. Dynamic or optional prefixes widen the types of .$tmpl and no-argument calls without losing route-parameter inference.
  • disableRegex (boolean | undefined, default false) — Skips validating the route templates when jetPaths() is called. Path and search values are still encoded.

Using both options: prepend adds the origin to every URL, and disableRegex allows the @ in /@me, which template validation would otherwise reject.

const Paths = jetPaths(
  {
    $path: '/api',
    Users: {
      $path: '/users',
      Me: '/@me', // "@" fails validation unless "disableRegex" is true
      One: '/:id',
    },
  },
  {
    prepend: 'https://example.com',
    disableRegex: true,
  },
);

Paths.Users.Me(); // "https://example.com/api/users/@me"
Paths.Users.Me({ fields: 'name' }); // "https://example.com/api/users/@me?fields=name"
Paths.Users.One({ id: 5 }); // "https://example.com/api/users/5"
Paths.Users.One.$tmpl; // "https://example.com/api/users/:id"

Validation and error cases

Route templates are validated once, when jetPaths() is called (skip this with disableRegex); values are validated on every call.

$path, $tmpl, and then can't be used as route names — then is reserved so route groups can safely pass through promises. Route groups must be plain objects with their own $path; null-prototype dictionaries work too, but class instances and other custom prototypes are rejected. Reusing a group of routes under multiple parents is fine, but nesting a group inside itself throws:

jetPaths({ $path: '/api', then: '/x' }); // ❌ "then" is a reserved key

const parent: any = { $path: '/parent' };
parent.Child = parent;
jetPaths(parent); // ❌ circular route definition at "Child"

Every path must start with / (only $path may be empty), static segments allow letters, numbers, -, ., _, ~ and percent-escapes but not . or .., path-variable names must be a whole segment, and fragments (#) or empty segments (//) aren't allowed:

jetPaths({ $path: '/api', Bad: '/../etc' }); // ❌ invalid template
jetPaths({ $path: '/api', Bad: '/:id-x' }); // ❌ ":id-x" isn't a whole segment

A path-variable name used more than once in a route is only passed once and inserted everywhere:

const DupPaths = jetPaths({ $path: '/api', Dup: '/:id/x/:id' });
DupPaths.Dup({ id: 5 }); // "/api/5/x/5"

Path values must be a primitive, and can't be '', '.', or '..', since they'd change the URL's structure; search values must be a primitive or an array of primitives. TypeScript rejects most invalid values, but the same checks run at runtime too, for values hidden behind a broader type or supplied by JavaScript callers:

Paths.Users.Delete({ id: '' }); // ❌ throws: would change the URL's structure
Paths.Users.Delete({ id: new Date() }); // ❌ throws: not a primitive

Paths.Users.Get({ since: new Date() }); // ❌ throws: not a primitive
Paths.Users.Get({ since: new Date().toISOString() }); // ✅

Passing undefined still counts as an argument, so a call with a missing value throws instead of returning the URL:

Paths.Users.Get(undefined); // ❌ throws, doesn't return the URL
Paths.Users.Get(); // ✅ "localhost:3000/api/users/all"

* * *

📄 License

MIT © seanpmaxwell

Happy web deving! 🚀

esm
http
javascript
nested-routes
nodejs
path-builder
query-params
rest-api
routes
search-params
type-inference
type-safe
typescript
url
url-builder

seanpmaxwell/jet-paths

Type-safe TypeScript utility for composing, validating, and formatting URL paths from nested objects — with inferred path and search params.

TypeScript

11

120 commits

updated Sep 29, 2026

See the code

See what people are saying

SourceMessageScoreDate

Typesafe formatting API endpoints with jet-paths (r/typescript)

Built this for my React + ExpressJS project a few years ago to keep all of my endpoints in one place and reduce some of the boiler-plate for adding path/search parameters. Up until recently only path parameters (and not search parameters) were type-safe. I just did a major update so now search…

1

Sep 29, 2026

README

✈️   jet-paths

npm version npm downloads TypeScript bundle size License

Recursively formats an object of URLs so that full paths are set up automatically, allowing you to insert path parameters and append search parameters easily and consistently.

* * *

👀 At a glance

Installation:

npm install jet-paths

Snippet:

const Paths = jetPaths({
  $path: '/api',
  Users: {
    $path: '/users',
    Get: '/all',
    One: '/:id',
  },
});

Paths.Users();                 // '/api/users'
Paths.Users.$path;             // '/users'

Paths.Users.Get();             // '/api/users/all'
Paths.Users.Get.$path;         // '/all'

Paths.Users.One({ id: 5 });    // '/api/users/5'
Paths.Users.One({ name: 5 });  // ❌ Type error: 'name' does not exist in type '{ id: Primitive }'
Paths.Users.One();             // ❌ Type error: expected at least one argument
Paths.Users.One.$path;         // '/:id'
Paths.Users.One.$tmpl;         // '/api/users/:id'

Paths.$path;                   // '/api'

Adding a path parameter (e.g. /:id) creates a type safe object which must be passed as the first argument to the URL function.

* * *

🤔 Why jet-paths?

  • Type-safe, single source of truth for all your routes
  • Full URLs out of the box with built-in type-safe formatting functions
  • Path and search params are type-checked, validated at runtime, and URL-encoded
  • .$path and .$tmpl give you the local and full unformatted path templates
  • Small, lightweight, and zero dependency: 2.1 kB gzipped + minified

* * *

⚡ Tutorial

Another, more complete snippet

import jetPaths from 'jet-paths';

const Paths = jetPaths(
  {
    $path: '/api',
    Users: {
      $path: '/users',
      Get: '/all',
      Add: '/add',
      Update: '/update',
      Delete: '/delete/:id',
    },
    Posts: {
      $path: '/posts',
      Get: '/all?<q!><page>', // <- search parameters
      Add: '/add',
      Update: '/update',
      Delete: '/delete/:id', // <- path parameters
      Private: {
        $path: '/private',
        Get: '/all',
        Delete: '/delete/:foo/bar/:id', // <- multiple path parameters
      },
    },
  },
  { prepend: 'localhost:3000' },
);

The object above is formatted into type-safe routes:

Paths.Users();                                    // "localhost:3000/api/users"
Paths.Users.$path;                                // "/users"
Paths.Users.Get({ page: 2 });                     // "localhost:3000/api/users/all?page=2"
Paths.Users.Delete({ id: 1 });                    // "localhost:3000/api/users/delete/1"
Paths.Users.Delete.$tmpl;                         // "localhost:3000/api/users/delete/:id"
Paths.Posts.Get({ q: 'x', page: 2 });             // "localhost:3000/api/posts/all?q=x&page=2"
Paths.Posts.Get({ q: 'x', pg: 3 });               // ❌ type error, pg is not declared
Paths.Posts.Private.Delete({ foo: 'a', id: 2 });  // "localhost:3000/api/posts/private/delete/a/bar/2"
Paths.Posts.Private.Delete({ foo: 'a' });         // ❌ type error, id is missing

If you store the definition in a variable before passing it to jetPaths, use as const to preserve the literal templates. TypeScript rejects widened string templates because their parameter names cannot be inferred safely:

const definition = {
  $path: '/api',
  One: '/users/:id',
} as const;

const UserPaths = jetPaths(definition);
UserPaths.One({ id: 5 }); // "/api/users/5"

Insert path parameters

Insertion functions always require path parameters as the first argument. Pass an optional second object to append search parameters:

// Insert path parameters without appending search parameters.
Paths.Users.Delete({ id: 1 });
// "localhost:3000/api/users/delete/1"

// Insert the same path parameters and append search parameters.
Paths.Users.Delete({ id: 1 }, { permanent: true });
// "localhost:3000/api/users/delete/1?permanent=true"

// The same applies to nested routes with multiple path parameters.
Paths.Posts.Private.Delete({ foo: 'a', id: 2 });
// "localhost:3000/api/posts/private/delete/a/bar/2"

Paths.Posts.Private.Delete({ foo: 'a', id: 2 }, { tags: ['draft', 'old'] });
// "localhost:3000/api/posts/private/delete/a/bar/2?tags=draft&tags=old"

An insertion function's .$tmpl keeps the placeholders intact. Missing or unknown path parameters are TypeScript errors, and throw at runtime:

Paths.Users.Delete.$tmpl; // "localhost:3000/api/users/delete/:id"

// @ts-expect-error - the path parameters object is required
Paths.Users.Delete();

// @ts-expect-error - "id" is missing
Paths.Users.Delete({}, { permanent: true });

// @ts-expect-error - the parameter is named "id", not "userId"
Paths.Users.Delete({ userId: 1 });

Appending search parameters without declaring them

For routes without path parameters, pass search parameters as the first argument, or no arguments when you don't need a query string. Arrays become repeated keys, undefined values are omitted, and false and 0 are preserved:

Paths.Users.Get();
// "localhost:3000/api/users/all"

Paths.Users.Get({
  tags: ['admin', 'editor'],
  page: 0,
  active: false,
  q: undefined,
});
// "localhost:3000/api/users/all?tags=admin&tags=editor&page=0&active=false"

// Group routes can append search parameters too.
Paths.Users({ page: 2 });
// "localhost:3000/api/users?page=2"

Pass unencoded values: path and search parameters are encoded automatically. Search parameter values must be primitives or arrays of primitives; convert dates to strings first.

Paths.Users.Delete({ id: 'team/a' }, { reason: 'duplicate entry' });
// "localhost:3000/api/users/delete/team%2Fa?reason=duplicate%20entry"

Paths.Users.Get({ since: new Date('2026-01-01T00:00:00Z').toISOString() });
// "localhost:3000/api/users/all?since=2026-01-01T00%3A00%3A00.000Z"

Declaring search parameters

To type-check a route's search parameters, list their keys after a ? in the route, each in angle brackets. Keys are optional unless they end with ! (i.e. <q!>). Only declared keys are accepted, and required keys must have a value:

const UserPaths = jetPaths({
  $path: '/api',
  Users: {
    $path: '/users',
    Search: '/search?<q!><page><sort>',
    One: '/:id?<expand>',
  },
});

UserPaths.Users.Search({ q: 'sean', page: 2 });
// "/api/users/search?q=sean&page=2"

UserPaths.Users.One({ id: 5 }, { expand: true });
// "/api/users/5?expand=true"

// A required key means the search params are required too, so the path
// template is on "$tmpl". The declared keys aren't part of it.
UserPaths.Users.Search.$tmpl; // "/api/users/search"

// @ts-expect-error - the search params are required
UserPaths.Users.Search();

// @ts-expect-error - "q" is required
UserPaths.Users.Search({ page: 2 });

// @ts-expect-error - "pgae" isn't declared
UserPaths.Users.Search({ q: 'sean', pgae: 2 });

Routes without declared keys accept any search parameters. Undeclared and missing keys also throw at runtime, for JavaScript callers and values hidden behind broader types.

To split a long declaration across lines, use a template literal with a \ at the end of each line; it joins the lines without adding a newline. Don't join strings with +: TypeScript types the result as string, which jetPaths rejects.

const FlightPaths = jetPaths({
  $path: '/api',
  Search: `/flights?<from!><to!><depart!><adults!>\
<return><children><infants><cabin><stops><airline><currency><sort>`,
});

FlightPaths.Search({ from: 'LHR', to: 'JFK', depart: '2026-10-01', adults: 2 });
// "/api/flights?from=LHR&to=JFK&depart=2026-10-01&adults=2"

Inherit path parameters from parents

A parameter in a parent's $path is required by that group and all its children, even when a child's local path has no placeholders:

const OrgPaths = jetPaths({
  $path: '/api',
  Org: {
    $path: '/orgs/:orgId',
    Members: '/members',
    Member: '/members/:memberId',
  },
});

OrgPaths.Org({ orgId: 7 });
// "/api/orgs/7"

OrgPaths.Org.Members({ orgId: 7 }, { page: 2 });
// "/api/orgs/7/members?page=2"

OrgPaths.Org.Member({ orgId: 7, memberId: 42 });
// "/api/orgs/7/members/42"

OrgPaths.Org.Members.$path; // "/members"
OrgPaths.Org.Members.$tmpl; // "/api/orgs/:orgId/members"

Group routes without adding a path segment

Use $path: '' when a group should organize your code without changing the URL:

const PublicPaths = jetPaths({
  $path: '/api',
  Public: {
    $path: '',
    Health: '/health',
    Status: '/status',
  },
});

PublicPaths.Public.Health(); // "/api/health"
PublicPaths.Public.Status(); // "/api/status"

Destructure routes and paths

Destructuring is safe: route functions don't rely on this, so they work the same after being pulled off their parent, and a destructured $path or $tmpl keeps its exact type:

const { Get, Delete } = Paths.Users;
Get(); // "localhost:3000/api/users/all"
Get({ page: 2 }); // "localhost:3000/api/users/all?page=2"
Delete({ id: 1 }); // "localhost:3000/api/users/delete/1"

const { $path } = Paths.Users.Get; // "/all"

Object rest and spread ({ ...Paths.Users }) only copy a function's properties, so the result is a plain object, not a callable route.


Options: prepend and disableRegex

  • prepend (string | undefined, default undefined) — Prepends a string verbatim to every generated URL and complete path template. This prefix is not validated or interpolated: put /:name parameters in the route definitions, not in prepend. Dynamic or optional prefixes widen the types of .$tmpl and no-argument calls without losing route-parameter inference.
  • disableRegex (boolean | undefined, default false) — Skips validating the route templates when jetPaths() is called. Path and search values are still encoded.

Using both options: prepend adds the origin to every URL, and disableRegex allows the @ in /@me, which template validation would otherwise reject.

const Paths = jetPaths(
  {
    $path: '/api',
    Users: {
      $path: '/users',
      Me: '/@me', // "@" fails validation unless "disableRegex" is true
      One: '/:id',
    },
  },
  {
    prepend: 'https://example.com',
    disableRegex: true,
  },
);

Paths.Users.Me(); // "https://example.com/api/users/@me"
Paths.Users.Me({ fields: 'name' }); // "https://example.com/api/users/@me?fields=name"
Paths.Users.One({ id: 5 }); // "https://example.com/api/users/5"
Paths.Users.One.$tmpl; // "https://example.com/api/users/:id"

Validation and error cases

Route templates are validated once, when jetPaths() is called (skip this with disableRegex); values are validated on every call.

$path, $tmpl, and then can't be used as route names — then is reserved so route groups can safely pass through promises. Route groups must be plain objects with their own $path; null-prototype dictionaries work too, but class instances and other custom prototypes are rejected. Reusing a group of routes under multiple parents is fine, but nesting a group inside itself throws:

jetPaths({ $path: '/api', then: '/x' }); // ❌ "then" is a reserved key

const parent: any = { $path: '/parent' };
parent.Child = parent;
jetPaths(parent); // ❌ circular route definition at "Child"

Every path must start with / (only $path may be empty), static segments allow letters, numbers, -, ., _, ~ and percent-escapes but not . or .., path-variable names must be a whole segment, and fragments (#) or empty segments (//) aren't allowed:

jetPaths({ $path: '/api', Bad: '/../etc' }); // ❌ invalid template
jetPaths({ $path: '/api', Bad: '/:id-x' }); // ❌ ":id-x" isn't a whole segment

A path-variable name used more than once in a route is only passed once and inserted everywhere:

const DupPaths = jetPaths({ $path: '/api', Dup: '/:id/x/:id' });
DupPaths.Dup({ id: 5 }); // "/api/5/x/5"

Path values must be a primitive, and can't be '', '.', or '..', since they'd change the URL's structure; search values must be a primitive or an array of primitives. TypeScript rejects most invalid values, but the same checks run at runtime too, for values hidden behind a broader type or supplied by JavaScript callers:

Paths.Users.Delete({ id: '' }); // ❌ throws: would change the URL's structure
Paths.Users.Delete({ id: new Date() }); // ❌ throws: not a primitive

Paths.Users.Get({ since: new Date() }); // ❌ throws: not a primitive
Paths.Users.Get({ since: new Date().toISOString() }); // ✅

Passing undefined still counts as an argument, so a call with a missing value throws instead of returning the URL:

Paths.Users.Get(undefined); // ❌ throws, doesn't return the URL
Paths.Users.Get(); // ✅ "localhost:3000/api/users/all"

* * *

📄 License

MIT © seanpmaxwell

Happy web deving! 🚀

esm
http
javascript
nested-routes
nodejs
path-builder
query-params
rest-api
routes
search-params
type-inference
type-safe
typescript
url
url-builder

Languages

TypeScript

97.9%

JavaScript

2.1%