<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sean Maxwell</title>
    <description>The latest articles on DEV Community by Sean Maxwell (@spmaxwell7).</description>
    <link>https://dev.to/spmaxwell7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4132192%2F81cb6955-d2dd-4717-917e-9e95dc564fe4.png</url>
      <title>DEV Community: Sean Maxwell</title>
      <link>https://dev.to/spmaxwell7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spmaxwell7"/>
    <language>en</language>
    <item>
      <title>Build Type-Safe URLs from a Route Tree with jet-paths</title>
      <dc:creator>Sean Maxwell</dc:creator>
      <pubDate>Tue, 29 Sep 2026 19:02:28 +0000</pubDate>
      <link>https://dev.to/spmaxwell7/build-type-safe-urls-from-a-route-tree-with-jet-paths-30pd</link>
      <guid>https://dev.to/spmaxwell7/build-type-safe-urls-from-a-route-tree-with-jet-paths-30pd</guid>
      <description>&lt;p&gt;Most projects start with a few URL strings. Before long, you have &lt;code&gt;`/api/users/${id}`&lt;/code&gt; in one file, &lt;code&gt;'/api/users/' + userId&lt;/code&gt; in another, and a route that still points to an endpoint you renamed months ago.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/jet-paths" rel="noopener noreferrer"&gt;jet-paths&lt;/a&gt; gives those URLs one place to live. You define a nested route tree, then call its routes like functions. TypeScript checks the parameters, and the library handles joining segments and encoding values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a route tree
&lt;/h2&gt;

&lt;p&gt;Install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;jet-paths
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define your paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jetPaths&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jet-paths&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;One&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/delete/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each route is callable, including the &lt;code&gt;Users&lt;/code&gt; group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                &lt;span class="c1"&gt;// '/api/users'&lt;/span&gt;
&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// '/api/users/all'&lt;/span&gt;
&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;   &lt;span class="c1"&gt;// '/api/users/5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't have to join &lt;code&gt;/api&lt;/code&gt;, &lt;code&gt;/users&lt;/code&gt;, and &lt;code&gt;/:id&lt;/code&gt; yourself. And because &lt;code&gt;One&lt;/code&gt; contains &lt;code&gt;:id&lt;/code&gt;, TypeScript knows the call needs an &lt;code&gt;id&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;            &lt;span class="c1"&gt;// Type error: missing argument&lt;/span&gt;
&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Type error: expected `id`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes you need the template rather than a finished path, such as when registering a server route. Both the local segment and the complete template are available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;One&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// '/:id'&lt;/span&gt;
&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;One&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$tmpl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// '/api/users/:id'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you put the definition in a separate variable, use &lt;code&gt;as const&lt;/code&gt; to preserve the literal route strings. Otherwise, TypeScript widens them to &lt;code&gt;string&lt;/code&gt; and can't infer names such as &lt;code&gt;id&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;One&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;definition&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;UserPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// '/api/users/5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add path and search parameters
&lt;/h2&gt;

&lt;p&gt;Path parameters go in the first argument. You can pass search parameters in a second argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/delete/5'&lt;/span&gt;

&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;permanent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/delete/5?permanent=true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a route without path parameters, the first argument is the search parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/all?page=2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can pass arrays, too. They become repeated keys. &lt;code&gt;undefined&lt;/code&gt; values are omitted, while &lt;code&gt;false&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt; are kept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/all?tags=admin&amp;amp;tags=editor&amp;amp;page=0&amp;amp;active=false'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass values as they are; jet-paths encodes them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;team/a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;duplicate entry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/delete/team%2Fa?reason=duplicate%20entry'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search values must be primitives or arrays of primitives. For something like a &lt;code&gt;Date&lt;/code&gt;, convert it to a string first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Declare query parameters when you want stricter types
&lt;/h2&gt;

&lt;p&gt;If a route accepts a known set of search parameters, you can put their names in the route definition. Add &lt;code&gt;!&lt;/code&gt; to make one required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search?&amp;lt;q!&amp;gt;&amp;lt;page&amp;gt;&amp;lt;sort&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;One&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id?&amp;lt;expand&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;UserPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/search?q=sean&amp;amp;page=2'&lt;/span&gt;

&lt;span class="nx"&gt;UserPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/users/5?expand=true'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;q&lt;/code&gt; is required; &lt;code&gt;page&lt;/code&gt; and &lt;code&gt;sort&lt;/code&gt; are optional. TypeScript also catches misspelled or undeclared keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;UserPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;            &lt;span class="c1"&gt;// Type error: missing `q`&lt;/span&gt;
&lt;span class="nx"&gt;UserPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pgae&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Type error: unknown key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The declarations describe the query parameters; they aren't included in the path template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;UserPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$tmpl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// '/api/users/search'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can leave query parameters undeclared when a route needs to accept arbitrary keys. Declared keys are also checked at runtime, which helps when values come from JavaScript or escape TypeScript's checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nest routes without repeating parameters
&lt;/h2&gt;

&lt;p&gt;Groups can have path parameters of their own. Child routes inherit them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OrgPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orgs/:orgId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/members&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Member&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/members/:memberId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;OrgPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Members&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/orgs/7/members?page=2'&lt;/span&gt;

&lt;span class="nx"&gt;OrgPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Member&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;memberId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// '/api/orgs/7/members/42'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Members&lt;/code&gt; doesn't mention &lt;code&gt;orgId&lt;/code&gt; in its own segment, but it still requires it because the parent route does.&lt;/p&gt;

&lt;p&gt;You can also use &lt;code&gt;$path: ''&lt;/code&gt; to organize routes in code without adding a URL segment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PublicPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Public&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;PublicPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Health&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// '/api/health'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Add an origin when you need one
&lt;/h2&gt;

&lt;p&gt;The optional &lt;code&gt;prepend&lt;/code&gt; setting adds a string to every generated path and complete template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;One&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// 'https://example.com/api/users/5'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prepend&lt;/code&gt; is added as written. Put dynamic parameters in the route tree, not in the prefix.&lt;/p&gt;

&lt;p&gt;There's also a &lt;code&gt;disableRegex&lt;/code&gt; option for skipping route-template validation. For example, it allows a static segment such as &lt;code&gt;/@me&lt;/code&gt;, which the default validator rejects. Values passed to route functions are still encoded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens with invalid values?
&lt;/h2&gt;

&lt;p&gt;jet-paths validates route templates when you create the tree and checks values when you call a route. A path value can't be empty, &lt;code&gt;.&lt;/code&gt; or &lt;code&gt;..&lt;/code&gt;, because those values would change the structure of the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Throws at runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Path values must be primitives, and search values must be primitives or arrays of primitives. Those runtime checks are useful when callers use JavaScript or pass values whose types are broader than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it in React
&lt;/h2&gt;

&lt;p&gt;Create the route tree once at module level, then import it where you need it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// paths.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jetPaths&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jet-paths&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jetPaths&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;One&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// UserLink.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Paths&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./paths&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserLink&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;One&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;User &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps route creation out of the component's render cycle. Calling an existing route to build a URL is cheap, so the link doesn't need special memoization.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;A route tree makes it easier to see which URLs your app uses and to change them in one place. With jet-paths, that definition also gives you checked path parameters, optional strict query parameters, encoding, and runtime validation.&lt;/p&gt;

&lt;p&gt;If your project has URL strings scattered across components and API calls, you can start by moving a small group of routes into one tree and build from there.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/jet-paths" rel="noopener noreferrer"&gt;jet-paths on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-paths" rel="noopener noreferrer"&gt;Source code on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I got tired of lining up comment dividers by hand, so I automated it</title>
      <dc:creator>Sean Maxwell</dc:creator>
      <pubDate>Fri, 25 Sep 2026 04:44:15 +0000</pubDate>
      <link>https://dev.to/spmaxwell7/i-got-tired-of-lining-up-comment-dividers-by-hand-so-i-automated-it-2mi1</link>
      <guid>https://dev.to/spmaxwell7/i-got-tired-of-lining-up-comment-dividers-by-hand-so-i-automated-it-2mi1</guid>
      <description>&lt;p&gt;This is a personal preference thing, but I like to write code top-down. Because of the way hoisting works in TypeScript/JavaScript, I can put the high-level stuff first and the details further down, so I split most files into regions in the same order every time:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Constants → Types → Classes (if any) → Functions → Export&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To keep those regions easy to spot when I'm scrolling, I mark them with dividers like these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ========================================================================= //&lt;/span&gt;
&lt;span class="c1"&gt;//                                 CONSTANTS                                 //&lt;/span&gt;
&lt;span class="c1"&gt;// ========================================================================= //&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ========================================================================= //&lt;/span&gt;
&lt;span class="c1"&gt;//                                 FUNCTIONS                                 //&lt;/span&gt;
&lt;span class="c1"&gt;// ========================================================================= //&lt;/span&gt;

&lt;span class="c1"&gt;// ============================= Shared Helpers ============================ //&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Big three-line headers for regions, and a single line for smaller sections inside a region.&lt;/p&gt;

&lt;h2&gt;
  
  
  The annoying part
&lt;/h2&gt;

&lt;p&gt;Writing these by hand is tedious. Every label is a different length, so every header needs a different amount of &lt;code&gt;=&lt;/code&gt; on each side to stay centered and end at the same column. I was copying and pasting old dividers, then deleting and adding &lt;code&gt;=&lt;/code&gt; until the closing &lt;code&gt;//&lt;/code&gt; lined up again.&lt;/p&gt;

&lt;p&gt;I first wrote a small script for myself that ran when I hit save. Then I needed it on my work computer, then in another project, then for a Python file. Instead of copy-pasting the script everywhere, I turned it into an npm package: &lt;a href="https://www.npmjs.com/package/code-divider" rel="noopener noreferrer"&gt;code-divider&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;You write a marker on its own line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// @reg constants&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// @reg functions&lt;/span&gt;

&lt;span class="c1"&gt;// @sec shared helpers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx code-divider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The markers are replaced in place with the headers from the top of this post. &lt;code&gt;@reg&lt;/code&gt; creates a region, and &lt;code&gt;@sec&lt;/code&gt; creates a section. By default, region labels become UPPERCASE and section labels become Capitalized Words, and every line ends at column 79.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5fnn75sowkacv4w80wb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5fnn75sowkacv4w80wb.gif" alt="code-divider inserting two region headers and a section header" width="800" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With no options it processes the current directory recursively. It skips things like &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;dist&lt;/code&gt;, and &lt;code&gt;.json&lt;/code&gt; files by default. You can also point it at one file or folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx code-divider &lt;span class="nt"&gt;--path&lt;/span&gt; ./src/index.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;--dry-run&lt;/code&gt; to see which files would change without touching anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on save
&lt;/h2&gt;

&lt;p&gt;This is how I actually use it. I write a marker, hit save, and the header appears.&lt;/p&gt;

&lt;p&gt;In VS Code, install the &lt;a href="https://github.com/emeraldwalk/vscode-runonsave" rel="noopener noreferrer"&gt;Run on Save&lt;/a&gt; extension and add this to your &lt;code&gt;settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"emeraldwalk.runonsave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commands"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;.(css|js|jsx|ts|tsx)$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx code-divider"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any editor that can run a command on save works the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Why not just use snippets?"
&lt;/h2&gt;

&lt;p&gt;Someone asked me this after my first post, and it's a fair question. Snippets can insert a header template, and if you're happy with that, keep using them. The catch is that snippets can change a label's case, but they can't count its characters. The filler stays the same length, so the line gets longer as the label gets longer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ================================= Helpers ================================= //&lt;/span&gt;
&lt;span class="c1"&gt;// ================================= Shared Helpers ================================= //&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;code-divider sizes the filler around each label, so every header ends at the same column. It also works in any editor, or in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other languages
&lt;/h2&gt;

&lt;p&gt;It isn't just for JavaScript. It ships with support for TypeScript, Java, CSS, SCSS, C, C++, Go, Rust, PHP, Ruby, Python, Bash, and SQL, and it uses each language's comment syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# @sec helpers
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ================================= Helpers ================================= #
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change the defaults, generate a config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx code-divider &lt;span class="nt"&gt;--init&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes a &lt;code&gt;code-divider.config.json&lt;/code&gt; with every default setting. You only need to keep the settings you want to change. For example, wider headers with &lt;code&gt;-&lt;/code&gt; as the filler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"All"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CharacterLimit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FillerCharacter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also give a language its own settings, or add a language that isn't built in by giving it file extensions and a comment syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it in CI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--check&lt;/code&gt; works like &lt;code&gt;--dry-run&lt;/code&gt;, but exits with code &lt;code&gt;1&lt;/code&gt; if any file would change. I use it to catch markers I forgot to format before they get committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx code-divider &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why I bother
&lt;/h2&gt;

&lt;p&gt;If you don't like dividing code this way, that's fine, and this tool isn't for you. But I find that clearly separated regions make files faster to scan, both for me and for teammates reading my code. I've also found that AI coding tools seem to do better with files that are organized into obvious sections.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to hear what you think. Bug reports, feature requests, and new language configs are all welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/code-divider" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/code-divider&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/seanpmaxwell/code-divider" rel="noopener noreferrer"&gt;https://github.com/seanpmaxwell/code-divider&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a unique ID generator that's ~60% faster than nanoid</title>
      <dc:creator>Sean Maxwell</dc:creator>
      <pubDate>Sat, 19 Sep 2026 17:37:39 +0000</pubDate>
      <link>https://dev.to/spmaxwell7/i-built-a-unique-id-generator-thats-60-faster-than-nanoid-1aan</link>
      <guid>https://dev.to/spmaxwell7/i-built-a-unique-id-generator-thats-60-faster-than-nanoid-1aan</guid>
      <description>&lt;p&gt;&lt;strong&gt;jet-id&lt;/strong&gt; is a tiny, zero-dependency unique ID generator for JavaScript and TypeScript. IDs are 28-character Crockford base32 strings, with optional timestamps so they sort by creation time. There's also a strictly ordered variant for when sorting by the millisecond isn't enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jetid&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jet-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;       &lt;span class="c1"&gt;// '9Q8SWWBTY-7NVXM-FT9S6-XB4R3M'&lt;/span&gt;
&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// '1KKNTQ2CN-606ZG-8VF48-76B6F3'  (sorts by creation time)&lt;/span&gt;
&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;    &lt;span class="c1"&gt;// '1M308A0DM-PR0000-422PKHWX-THHP6B3G-DPQCJ4GCE'  (sorts within the millisecond too)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/seanpmaxwell" rel="noopener noreferrer"&gt;
        seanpmaxwell
      &lt;/a&gt; / &lt;a href="https://github.com/seanpmaxwell/jet-id" rel="noopener noreferrer"&gt;
        jet-id
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An extremely fast unique ID generator for JavaScript and TypeScript, with optional sorting/timestamping.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;jet-id&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/jet-id" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/76581b732f975b2cf4ed4383ccecc27efcf0537e6b02521ea88ad9ff3bdc2f91/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f6a65742d69643f7374796c653d666c61742d737175617265266c6f676f3d6e706d266c6f676f436f6c6f723d776869746526636f6c6f723d636233383337" alt="npm"&gt;&lt;/a&gt;
&lt;a href="https://github.com/seanpmaxwell/jet-id/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/20525214c1f31ffb8f6a8c4358242aae04b9ba4734a794f11d4c7d45395180f0/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7365616e706d617877656c6c2f6a65742d69642f63692e796d6c3f7374796c653d666c61742d737175617265266c6f676f3d676974687562266c6f676f436f6c6f723d7768697465266c6162656c3d4349" alt="CI"&gt;&lt;/a&gt;
&lt;a href="https://github.com/seanpmaxwell/jet-id/./LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/ac049ef4e7a0b7196b09add6ac2d4f180e544c0ac779c2b2ac2fd2723a209579/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75653f7374796c653d666c61742d737175617265" alt="license"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;An extremely fast unique ID generator for JavaScript and TypeScript, with optional timestamping and sorting.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install jet-id&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-ts notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;jetId&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'jet-id'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-en"&gt;jetId&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt; &lt;span class="pl-c"&gt;// '9Q8SWWBTY-7NVXM-FT9S6-XB4R3M'&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;A plain ID contains 25 random Crockford base32 characters, grouped as &lt;code&gt;9-5-5-6&lt;/code&gt;. Including the three dashes, each ID is 28 characters long.&lt;/p&gt;
&lt;p&gt;· · ·&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Table of Contents&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#why-jet-id" rel="noopener noreferrer"&gt;Why jet-id?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#api" rel="noopener noreferrer"&gt;API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/seanpmaxwell/jet-id#jetid" rel="noopener noreferrer"&gt;jetId&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetid-1" rel="noopener noreferrer"&gt;&lt;code&gt;jetId()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidtest" rel="noopener noreferrer"&gt;&lt;code&gt;jetId.test()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidtimed" rel="noopener noreferrer"&gt;&lt;code&gt;jetId.timed()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidtimedparse" rel="noopener noreferrer"&gt;&lt;code&gt;jetId.timed.parse()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidmono" rel="noopener noreferrer"&gt;jetId.mono&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidmono-1" rel="noopener noreferrer"&gt;&lt;code&gt;jetId.mono()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidmonotest" rel="noopener noreferrer"&gt;&lt;code&gt;jetId.mono.test()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetidmonoparse" rel="noopener noreferrer"&gt;&lt;code&gt;jetId.mono.parse()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/seanpmaxwell/jet-id#jetkey" rel="noopener noreferrer"&gt;jetKey&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#jetkey-1" rel="noopener noreferrer"&gt;&lt;code&gt;jetKey()&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/seanpmaxwell/jet-id#command-line" rel="noopener noreferrer"&gt;Command line&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#options" rel="noopener noreferrer"&gt;Options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#generator-types" rel="noopener noreferrer"&gt;Generator types&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#benchmarks" rel="noopener noreferrer"&gt;Benchmarks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/seanpmaxwell/jet-id#license" rel="noopener noreferrer"&gt;License&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;· · ·&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why jet-id?&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast:&lt;/strong&gt; Faster than &lt;code&gt;nanoid()&lt;/code&gt; in the included &lt;a href="https://github.com/seanpmaxwell/jet-id#benchmarks" rel="noopener noreferrer"&gt;benchmarks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small:&lt;/strong&gt; 6.9 kB packed, with zero runtime dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript-ready:&lt;/strong&gt; Includes type declarations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable:&lt;/strong&gt; Works in Node.js and modern browsers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible:&lt;/strong&gt; Choose random IDs, timestamped IDs, strictly ordered IDs, or secret keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong randomness:&lt;/strong&gt; &lt;code&gt;jetId()&lt;/code&gt; and &lt;code&gt;jetId.mono()&lt;/code&gt; provide 125 random bits. For comparison, UUID v4 has 122.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple API:&lt;/strong&gt; Generate IDs, validate their…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/seanpmaxwell/jet-id" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~81 million IDs/sec&lt;/strong&gt;, vs. nanoid's ~53 million on my machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;125 random bits&lt;/strong&gt; from the platform CSPRNG — &lt;code&gt;randomFillSync&lt;/code&gt; in Node, &lt;code&gt;crypto.getRandomValues&lt;/code&gt; in browsers (UUID v4 has 122)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strictly ordered when you need it:&lt;/strong&gt; &lt;code&gt;jetid.mono()&lt;/code&gt; orders IDs within a single millisecond without giving up any of those 125 bits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable:&lt;/strong&gt; the Crockford alphabet has no &lt;code&gt;I&lt;/code&gt;, &lt;code&gt;L&lt;/code&gt;, &lt;code&gt;O&lt;/code&gt;, or &lt;code&gt;U&lt;/code&gt; to confuse with &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small:&lt;/strong&gt; zero dependencies, 6.9 kB packed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal:&lt;/strong&gt; runs in Node.js and modern browsers, and there's a CLI too&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it started
&lt;/h2&gt;

&lt;p&gt;I was looking for an ID generator for a web project (mostly for a database column) that used the Crockford alphabet and had optional timestamping. I couldn't find anything that quite fit, so I debated between wrapping nanoid and writing my own. It seemed like a fun project that probably just involved some simple arithmetic (or so I naively thought), so I wrote my own.&lt;/p&gt;

&lt;p&gt;The first version took a morning. Then, out of curiosity, I fed it to &lt;strong&gt;Fable 5.1&lt;/strong&gt; and &lt;strong&gt;GPT Astra&lt;/strong&gt;, and I was blown away. They introduced me to techniques I didn't know existed, like pooling, lookup tables, and bit-shifting. I spent the next two days passing it back and forth between them, benchmarking every change, rolling back anything that got slower, and adding my own tweaks in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it fast
&lt;/h2&gt;

&lt;p&gt;Most of the speed comes from doing expensive work rarely and in bulk:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pooling.&lt;/strong&gt; IDs are built 256 at a time into a single string, so most calls to &lt;code&gt;jetid()&lt;/code&gt; are just a &lt;code&gt;substring&lt;/code&gt;. One call to the CSPRNG supplies enough randomness for 1,024 IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two characters per lookup.&lt;/strong&gt; A 1,024-entry table turns 10 random bits into two Crockford characters at once, instead of encoding one character at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four bytes at a time.&lt;/strong&gt; Each 28-character ID is written as seven 32-bit words through a &lt;code&gt;Uint32Array&lt;/code&gt;, dashes included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One string conversion per chunk.&lt;/strong&gt; The bytes are decoded to a string once (through &lt;code&gt;Buffer&lt;/code&gt; in Node, &lt;code&gt;TextDecoder&lt;/code&gt; in browsers), and every ID is sliced from it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cached timestamp.&lt;/strong&gt; For timed IDs, the nine-character timestamp is split into a six-character prefix that only changes every 32 seconds and a three-character suffix that changes each millisecond. The prefix is cached, so most calls re-encode three characters, not nine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;On my MacBook M4 Pro (Node 24), median of 7 samples of at least 500 ms each, after a 500 ms warmup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generator&lt;/th&gt;
&lt;th&gt;Chars&lt;/th&gt;
&lt;th&gt;Random bits&lt;/th&gt;
&lt;th&gt;Ops/sec&lt;/th&gt;
&lt;th&gt;Relative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jetid()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;td&gt;81,127,524&lt;/td&gt;
&lt;td&gt;1.00x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nanoid()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;126&lt;/td&gt;
&lt;td&gt;52,583,004&lt;/td&gt;
&lt;td&gt;0.65x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nanoid, Crockford, 25 chars&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;td&gt;46,942,586&lt;/td&gt;
&lt;td&gt;0.58x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nanoid, Crockford, 9-5-5-6&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;td&gt;15,589,642&lt;/td&gt;
&lt;td&gt;0.19x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jetid.mono()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;td&gt;13,035,960&lt;/td&gt;
&lt;td&gt;0.16x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crypto.randomUUID()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;td&gt;9,964,004&lt;/td&gt;
&lt;td&gt;0.12x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ulid (monotonic)&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;3,637,167&lt;/td&gt;
&lt;td&gt;274.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;uuid&lt;/code&gt; v4&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;122&lt;/td&gt;
&lt;td&gt;8,577,377&lt;/td&gt;
&lt;td&gt;0.11x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's roughly 50% faster than nanoid's defaults, even though jet-id's IDs are longer (28 characters vs. 21). Configured to produce the same &lt;code&gt;9-5-5-6&lt;/code&gt; format, nanoid is about 5x slower. To be fair, nanoid isn't built for dash-separated output, so part of that gap is the formatting wrapper.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jetid.mono()&lt;/code&gt; is the slowest of jet-id's own generators because it reads the high-resolution clock and encodes a sequence number on every call, rather than slicing a pre-built pool. It's still faster than either UUID option and ULID's monotonic option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 9-5-5-6?
&lt;/h2&gt;

&lt;p&gt;It looks a bit different, but it isn't arbitrary. The first segment is 9 characters because that's how long the current epoch (in milliseconds) is in Crockford base32, which leaves room for the optional timestamp without changing the ID's length. I wanted at least UUID v4's entropy, which takes 25 random characters, and splitting the remaining 16 into 5-5-6 seemed the most readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timestamped IDs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;jetid.timed()&lt;/code&gt; encodes the current time in the first nine characters, so plain string sorting is creation order. You can also pass your own epoch in milliseconds, and &lt;code&gt;jetid.timed.parse()&lt;/code&gt; reads it back out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2015&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// '19GAS9400-...'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;         &lt;span class="c1"&gt;// '1KKNTQ2CN-606ZG-8VF48-76B6F3'&lt;/span&gt;
&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// 1773480413589&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// 2026-03-14T09:26:53.589Z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two tradeoffs to know about: timestamped IDs spend those first 9 characters on the time, so they carry 80 random bits instead of 125, and IDs created in the same millisecond have no defined order between them. If that second one matters to you, keep reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strictly ordered IDs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;jetid.mono()&lt;/code&gt; is for when "sorts by creation time" has to mean &lt;em&gt;exactly&lt;/em&gt; — every ID sorts strictly after the one before it, even within a single millisecond.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jetid&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jet-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// '1M308A0DM-PR0000-422PKHWX-THHP6B3G-DPQCJ4GCE'&lt;/span&gt;
&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// '1M308A0DM-WR0000-1ZS1A6M5-NE6RKRHN-X9F480HCB'&lt;/span&gt;
&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// '1M308A0DM-X50000-XJ1ZENAH-MVX225N0-MZ0BTD8YB'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's 44 characters in a &lt;code&gt;9-6-8-8-9&lt;/code&gt; layout: the 9-character timestamp as before, then 6 characters of ordering key (10 bits of sub-millisecond time from &lt;code&gt;performance.now()&lt;/code&gt; plus a 20-bit counter for ties), then 25 random characters.&lt;/p&gt;

&lt;p&gt;That's why it's longer. Ordering costs 15 characters, and I didn't want to pay for them out of the randomness. At 40 characters the random tail keeps the full 125 bits, so &lt;code&gt;jetid.mono()&lt;/code&gt; is exactly as collision-resistant as &lt;code&gt;jetid()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two caveats. It takes no epoch argument — the ordering guarantee only holds against time it reads itself. And the guarantee is per process: the counter lives in module state, so IDs from separate workers or machines aren't ordered against each other.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jetid.mono.test()&lt;/code&gt; and &lt;code&gt;jetid.mono.parse()&lt;/code&gt; work like their &lt;code&gt;jetid&lt;/code&gt; counterparts; &lt;code&gt;parse&lt;/code&gt; returns &lt;code&gt;{ epoch, fraction, counter }&lt;/code&gt;. Use &lt;code&gt;epoch&lt;/code&gt; for dates. The other two are an ordering key, not a finer clock — browsers coarsen &lt;code&gt;performance.now()&lt;/code&gt;, so on Firefox and Safari &lt;code&gt;counter&lt;/code&gt; does nearly all the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating IDs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;jetid.test()&lt;/code&gt; checks whether a value is a well-formed ID. It isn't case-sensitive, and it accepts anything, returning &lt;code&gt;false&lt;/code&gt; rather than throwing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;avz6yg1rb-47j6r-xgns9-tqw29a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nx"&gt;jetid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                           &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jetid.mono.test()&lt;/code&gt; does the same for the 44-character format. Neither accepts the other's IDs.&lt;/p&gt;

&lt;h2&gt;
  
  
  From the command line
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx jet-id               &lt;span class="c"&gt;# one random ID&lt;/span&gt;
npx jet-id &lt;span class="nt"&gt;-c&lt;/span&gt; 5          &lt;span class="c"&gt;# five, one per line&lt;/span&gt;
npx jet-id &lt;span class="nt"&gt;-t&lt;/span&gt; timed      &lt;span class="c"&gt;# one timestamped ID&lt;/span&gt;
npx jet-id &lt;span class="nt"&gt;-t&lt;/span&gt; mono &lt;span class="nt"&gt;-c&lt;/span&gt; 10  &lt;span class="c"&gt;# ten strictly ordered IDs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;These benchmarks were all done on my local machine using the &lt;a href="https://github.com/seanpmaxwell/jet-id/blob/main/playground/benchmark.ts" rel="noopener noreferrer"&gt;&lt;code&gt;playground/benchmark.ts&lt;/code&gt;&lt;/a&gt; script. If they're different for you, or if you know of something faster, feedback is very welcome, especially if there's a flaw in the way I did the benchmarking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/seanpmaxwell/jet-id" rel="noopener noreferrer"&gt;https://github.com/seanpmaxwell/jet-id&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/jet-id" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/jet-id&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
