cadre-router is a ReScript-first routing library providing:
-
Type-safe route definitions — Routes are variants, not strings
-
Bidirectional serialization —
Route.t → stringandstring → Route.t -
Typed route parameters —
Journey(JourneyId.t)instead ofJourney(string) -
Elm-style parser combinators — Composable URL parsing DSL
-
Browser History API — Client-side navigation primitives
-
Framework-agnostic core — Works with React, rescript-tea, or vanilla
// Define your routes as a variant
type route =
| Home
| Profile
| Journey(JourneyId.t)
| NotFound
// Create a parser using combinators
let parser = {
open CadreRouter.Parser
oneOf([
top->map(_ => Home),
s("profile")->map(_ => Profile),
s("journey")->andThen(JourneyId.parser)->map(((_, id)) => Journey(id)),
])
}
// Serialize routes to URLs
let toString = route => switch route {
| Home => "/"
| Profile => "/profile"
| Journey(id) => "/journey/" ++ JourneyId.toString(id)
| NotFound => "/404"
}
// Parse current URL
let currentRoute = CadreRouter.Url.fromLocation()->CadreRouter.Parser.parse(parser, _)
// Navigate programmatically
module Nav = CadreRouter.Navigation.Make({ type t = route; let toString = toString })
Nav.pushRoute(Profile)Parsed URL representation.
type t = {
path: list<string>, // ["journey", "abc123"]
query: Belt.Map.String.t<string>, // {"tab": "map"}
fragment: option<string>, // Some("section")
}
let fromString: string => t
let fromLocation: unit => t // Read window.location
let toString: t => string
let getQueryParam: (t, string) => option<string>
let getQueryParamInt: (t, string) => option<int>Elm-style URL parser combinators.
type t<'a> // Parser producing 'a
// Segment matchers
let s: string => t<unit> // Literal: s("profile")
let str: t<string> // Any string segment
let int: t<int> // Integer segment
let custom: (string => option<'a>) => t<'a> // Custom (for typed IDs)
let top: t<unit> // End of path
// Combinators
let andThen: (t<'a>, t<'b>) => t<('a, 'b)> // Sequential
let \"</>": (t<'a>, t<'b>) => t<('a, 'b)> // Operator form
let map: (t<'a>, 'a => 'b) => t<'b> // Transform
let oneOf: array<t<'a>> => t<'a> // Alternatives
// Query params
let query: string => t<option<string>>
let queryInt: string => t<option<int>>
let queryRequired: string => t<string>
// Execute
let parse: (t<'a>, Url.t) => option<'a>Browser History API abstraction.
let pushUrl: string => unit
let replaceUrl: string => unit
let back: unit => unit
let forward: unit => unit
let currentUrl: unit => Url.t
let onUrlChange: (Url.t => unit) => unsubscribe
// Type-safe functor
module Make: (R: { type t; let toString: t => string }) => {
let pushRoute: R.t => unit
let replaceRoute: R.t => unit
}Type-safe link component for React applications.
// Generic href-based link
<CadreRouter.Link href="/profile">"Profile"</CadreRouter.Link>
// Type-safe route-based link (via functor)
module MyLink = CadreRouter.Link.Make({
type t = Route.t
let toString = Route.toString
})
<MyLink route={Route.Profile}>"Profile"</MyLink>For type-safe route parameters, define ID modules:
module JourneyId = {
type t = JourneyId(string)
let fromString = str =>
if Js.String2.length(str) > 0 { Some(JourneyId(str)) }
else { None }
let toString = (JourneyId(str)) => str
// Parser for cadre-router
let parser = CadreRouter.Parser.custom(fromString)
}
// Usage in route parser:
s("journey")->andThen(JourneyId.parser)->map(((_, id)) => Journey(id))Invalid IDs are rejected during URL parsing, not later in the app.
type journeySubRoute = JourneyMap | JourneyLog | JourneySettings
type route = Journey(JourneyId.t, journeySubRoute)
let subParser = Parser.oneOf([
Parser.s("map")->Parser.map(_ => JourneyMap),
Parser.s("log")->Parser.map(_ => JourneyLog),
Parser.s("settings")->Parser.map(_ => JourneySettings),
Parser.top->Parser.map(_ => JourneyMap), // default
])
// /journey/:id/map, /journey/:id/log, etc.
let parser =
Parser.s("journey")
->Parser.andThen(JourneyId.parser)
->Parser.andThen(subParser)
->Parser.map((((_, id), sub)) => Journey(id, sub))type route = Search({ query: string, page: option<int> })
let parser =
Parser.s("search")
->Parser.andThen(Parser.queryRequired("q"))
->Parser.andThen(Parser.queryInt("page"))
->Parser.map((((_, q), page)) => Search({ query: q, page }))
// Parses: /search?q=hello&page=2TEA integration is now built-in! (Merged from cadre-tea-router v0.2.0)
The src/tea/ modules provide complete TEA/Elm Architecture integration:
// Import TEA routing modules
open CadreRouter.Tea
// Define routes
type route = Home | Profile | Journey(JourneyId.t) | NotFound
// TEA-specific routing
let init = () => {
// Initialize from current URL
let route = Tea_Router.fromUrl(Url.fromLocation())
({route}, Cmd.none)
}
let update = (msg, model) => {
switch msg {
| UrlChanged(route) => ({...model, route}, Cmd.none)
| Navigate(newRoute) =>
(model, Tea_Navigation.push(routeToUrl(newRoute)))
}
}
let subscriptions = _ => {
// Subscribe to URL changes
Tea_Router.urlChanges(url => UrlChanged(parseRoute(url)))
}TEA Modules:
-
Tea_Router— URL → Msg patterns, route parsing -
Tea_Navigation—Tea.Cmd.twrappers for push/replace/back/forward -
Tea_Url— URL parsing utilities -
Tea_QueryParams— Query parameter helpers -
Tea_Guards— Route guards (block navigation during sync)
Installation for TEA apps:
npm install cadre-router rescript-teaAdd to rescript.json:
{
"bs-dependencies": ["cadre-router"]
}Note: rescript-tea is a peer dependency. If you’re only using the framework-agnostic core (React/vanilla), you don’t need it.
Use the Link module directly:
module RouteLink = CadreRouter.Link.Make({
type t = Route.t
let toString = Route.toString
})
@react.component
let make = () => {
<nav>
<RouteLink route={Route.Home}>"Home"</RouteLink>
<RouteLink route={Route.Profile}>"Profile"</RouteLink>
</nav>
}Use Navigation directly:
module Nav = CadreRouter.Navigation.Make({
type t = Route.t
let toString = Route.toString
})
// Navigate
Nav.pushRoute(Route.Profile)
// Listen for changes
let unsubscribe = CadreRouter.Navigation.onUrlChange(url => {
let route = url->CadreRouter.Parser.parse(Route.parser, _)
// Update your app state
})-
Complete API Guide — Comprehensive routing documentation covering type-safe routing concepts, parser combinators, navigation system, TEA integration, and complete examples
-
src/client/— Client-side routing modules (Url, Parser, Navigation, Link) -
src/— Server-side routing (future) -
examples/— Usage examples -
docs/— Design documents and specifications
-
Typed by default — Routes are variants, not strings
-
Bidirectional — Parse and serialize with the same type
-
Explicit — No hidden global state or magic
-
Composable — Small combinators, not monolithic config
-
Framework-agnostic — Core works anywhere, integrations are separate
This project uses:
-
ReScript (source of truth)
-
JavaScript (compiled output)
-
Web APIs / Deno runtime
It does not depend on:
-
Node.js
-
TypeScript
-
npm (use Deno imports)