Skip to content

Type-safe URL routing for ReScript applications with Elm-style parser combinators

License

Unknown, Unknown licenses found

Licenses found

Unknown
LICENSE
Unknown
LICENSE.txt
Notifications You must be signed in to change notification settings

hyperpolymath/cadre-router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

85 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

cadre-router

Palimpsest

Type-safe URL routing for ReScript applications.

What this is

cadre-router is a ReScript-first routing library providing:

  • Type-safe route definitions — Routes are variants, not strings

  • Bidirectional serializationRoute.t → string and string → Route.t

  • Typed route parametersJourney(JourneyId.t) instead of Journey(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

Quick Example

// 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)

Modules

Url

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>

Parser

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>

Navigation

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>

Typed ID Pattern

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.

Nested Routes

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))

Query Parameters

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=2

Framework Integration

rescript-tea (NEW: Integrated!)

TEA 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_NavigationTea.Cmd.t wrappers 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-tea

Add 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.

React

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>
}

Vanilla

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
})

Documentation

  • Complete API Guide — Comprehensive routing documentation covering type-safe routing concepts, parser combinators, navigation system, TEA integration, and complete examples

Repository Layout

  • src/client/ — Client-side routing modules (Url, Parser, Navigation, Link)

  • src/ — Server-side routing (future)

  • examples/ — Usage examples

  • docs/ — Design documents and specifications

Design Principles

  • 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

Technology Boundaries

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)

License

See LICENSE.txt.

Contributing

Contributions welcome:

  • Bug fixes and improvements

  • Additional parser combinators

  • Documentation improvements

  • Framework integration examples

AsciiDoc is preferred for documentation.

About

Type-safe URL routing for ReScript applications with Elm-style parser combinators

Topics

Resources

License

Unknown, Unknown licenses found

Licenses found

Unknown
LICENSE
Unknown
LICENSE.txt

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors