Unreleased changes
These pages document main, which has changes 2.1.1 does not. npm install @evanion/urn gives you 2.1.1, documented here. Values on these pages are produced by running the current source on every build.
Checking URN input
isValidFormat in @evanion/urn screens a URN from outside the shop before parse reads it, ensuring your application doesn’t crash on malformed input.
The Problem: Untrusted Input
When receiving URNs from external sources—API requests, URL parameters, or user input—you cannot assume they are well-formed. Attempting to parse a string that isn’t a URN will throw an error, which, if uncaught, can crash your request handler.
Checking every input with a try...catch block is verbose and computationally expensive for high-volume endpoints.
Solution: Pre-screening with isValidFormat
isValidFormat allows you to screen input without the overhead of exception handling. It internally calls parse but returns a boolean instead of throwing, ensuring that your validation logic and parsing logic never disagree.
GameURN screens a URN from outside the shop before reading it:
function (: string) {
return .() ? .() : null;
}
('urn:game:hive'); // -> { urn: 'urn', nid: 'game', nss: 'hive' }
('urn:game:spirit island'); // -> null
('hive'); // -> null
('urn:order:order-2026-0042'); // -> { urn: 'urn', nid: 'order', nss: 'order:order-2026-0042' }In this example, urnOrNull returns null for the space in spirit island, and for hive, which lacks both a scheme and a NID. It returns the order URN, because isValidFormat checks the shape and not the namespace.
Avoiding the “Unbound Static” Pitfall
A common JavaScript error occurs when passing static methods as callbacks. Every static method of an @evanion/urn class reads this to determine the current namespace. If you pass a static method as a bare callback, it loses its class binding.
stringify, parse and extractId will throw a TypeError in this scenario. However, isValidFormat, sameNamespace and equals are designed to fail gracefully and return false instead.
const = ['urn:game:azul', 'urn:game:hive'];
.(() => .()); // -> ['azul', 'hive']
let = '';
try {
.(.);
} catch () {
= ( as Error).;
}
; // -> 'TypeError'
.(.); // -> []
.(() => .()); // -> ['urn:game:azul', 'urn:game:hive']catalogue.filter(GameURN.isValidFormat) returns an empty list because the binding is lost. To fix this, wrap the call in an arrow function to preserve the class context, as the first map and the last filter do.
Distinguishing Malformed URNs from Bad Parts
@evanion/urn provides two distinct error classes to help you provide better feedback to your users.
ValidationError: The string is not a URN at all (e.g., it is missing theurn:scheme or the NID).InvalidError: The string is a URN, but a specific part (like the NSS) broke its grammar.
import { , , } from '@evanion/urn';
function (: string): string {
try {
.();
return 'none';
} catch () {
if ( instanceof ) return `bad ${.}`;
if ( instanceof ) return 'not a URN';
throw ;
}
}
('urn:game:azul'); // -> 'none'
('urn:game:spirit island'); // -> 'bad NSS'
('azul'); // -> 'not a URN'Every InvalidError is also a ValidationError. An InvalidError carries detailed fields to explain exactly what failed:
| Field | What it holds |
|---|---|
property | The failed part: 'URN', 'NID', 'NSS', 'R-COMPONENT', 'Q-COMPONENT' or 'F-COMPONENT' |
value | The part as it was written |
invalidChar | The first character outside the grammar (undefined if the whole part is fundamentally broken) |
reason | The rule broken when characters are valid (e.g., the NID is too short) |
Minting Batches Safely
Since stringify throws an InvalidError on a part that breaks its grammar, you should catch these errors when processing a batch of IDs. This ensures one bad slug doesn’t stop the entire process:
import { , } from '@evanion/urn';
function (: string[]): { : string[]; : string[] } {
const : string[] = [];
const : string[] = [];
for (const of ) {
try {
.(.({ : , : 'game' }));
} catch () {
if (!( instanceof )) throw ;
.();
}
}
return { , };
}
(['azul', 'spirit island', 'wingspan']); // -> { minted: ['urn:game:azul', 'urn:game:wingspan'], skipped: ['spirit island'] }URN Part Grammars
@evanion/urn validates each part against specific grammars: the NID and the NSS against RFC 8141, and the scheme against RFC 3986, so a scheme other than urn passes:
- Scheme: A letter, followed by letters, digits,
+,-and.. - NID: Letters, digits and interior hyphens.
stringifyrequires 2-32 characters;parseaccepts 1-32. - NSS: Letters, digits,
-._~!$&'()*+,;=:@, percent-encoded characters, and/after the first character. Spaces,?and#are prohibited.
schemeGrammar, nidGrammar and nssGrammar hold the patterns stringify checks. Note that the NID is the only part read and written differently: parse accepts a one-character NID (per RFC 2141), while stringify refuses to write it.
..('baize'); // -> true
..('game'); // -> true
..('g'); // -> false
.('urn:g:azul'); // -> true
..('brass-birmingham'); // -> true
..('brass birmingham'); // -> falseNext steps: Adding metadata
A URN that isValidFormat accepts has passed each @evanion/urn grammar. r-, q- and f-components shows how to add an edition and a section of the rules to urn:game:brass-birmingham, and reads both back with parse.