Skip to main content
Miso includes a built-in router that converts between URLs and Haskell data types without any string manipulation in application code. Routing is bidirectional: the same type that decodes a URL can re-encode it back to a string for use in links.

The Router typeclass

class Router route where
  -- | Encode a route value into a list of URL tokens
  fromRoute :: route -> [Token]

  -- | Decode a URL string into a route (or an error)
  toRoute :: MisoString -> Either RoutingError route

  -- | Pretty-print a route as a URL string
  prettyRoute :: route -> MisoString

  -- | Produce a type-safe href_ attribute
  href_ :: route -> Attribute action

  -- | Parse a URI value into a route
  route :: URI -> Either RoutingError route

  -- | The route parser (used internally and for manual construction)
  routeParser :: RouteParser route
You implement this class in one of two ways: manual instance definition, or Generic deriving.

Approach 1 — Manual instance

Define routeParser using the combinator DSL and define fromRoute to encode each constructor back to tokens:
data Route = Widget MisoString Int
  deriving (Show, Eq)

instance Router Route where
  routeParser = routes
    [ Widget <$> path "widget" <*> capture
    ]
  fromRoute (Widget p value) = [ toPath p, toCapture value ]
Parsing:
main :: IO ()
main = print (runRouter "/widget/10" routeParser)
-- Right (Widget "widget" 10)

Parser combinators

CombinatorTypeDescription
pathMisoString -> RouteParser MisoStringMatch a fixed path segment
captureFromMisoString a => RouteParser aParse a dynamic segment
queryParam(FromMisoString a, KnownSymbol p) => RouteParser (QueryParam p a)Parse a query parameter
queryFlagKnownSymbol p => RouteParser (QueryFlag p)Parse a boolean query flag
routes[RouteParser route] -> RouteParser routeTry alternatives in order

Token constructors

-- | Build a path segment token
toPath :: MisoString -> Token

-- | Build a capture variable token
toCapture :: ToMisoString string => string -> Token

-- | Build a query parameter token
toQueryParam :: ToMisoString s => MisoString -> s -> Token

Approach 2 — Generic deriving

Annotate your sum type with DeriveGeneric and DeriveAnyClass and derive Router automatically:
{-# LANGUAGE DeriveAnyClass  #-}
{-# LANGUAGE DeriveGeneric   #-}
{-# LANGUAGE DerivingStrategies #-}

data Route
  = About
  | Home
  | Widget (Capture "thing" Int) (Path "foo") (Capture "other" MisoString) (QueryParam "bar" Int)
  deriving stock (Generic, Show)
  deriving anyclass Router
The Generic deriver:
  • Converts the constructor name to a lowercase path segment (Widget"widget").
  • Processes Capture, Path, QueryParam, and QueryFlag fields in declaration order.
  • The Index constructor name is special — it encodes the "/" root path.
  • Camel-case names use only the first lowercase hump (FooBar"foo").
test :: Either RoutingError Route
test = toRoute "/widget/23/foo/okay?bar=0"
-- Right (Widget (Capture 23) (Path "foo") (Capture "okay") (QueryParam (Just 0)))
Re-encoding:
prettyRoute $ Widget (Capture 23) (Path "foo") (Capture "okay") (QueryParam (Just 0))
-- "/widget/23/foo/okay?bar=0"

Route types for Generic deriving

TypeKindPurpose
Capture sym anewtypeDynamic path segment, decoded from/to a
Path pathnewtypeFixed path segment declared at the type level
QueryParam path anewtypeOptional query parameter ?path=value
QueryFlag pathnewtypePresence/absence flag ?path
The order of Capture and Path fields in the constructor determines the order of path segments in the URL. The order of QueryParam and QueryFlag fields does not affect the URL — query parameters can appear in any order.
The href_ function converts a route value directly into an Attribute action, so you never construct URL strings by hand:
import Miso.Router (href_)

viewNav :: View model action
viewNav =
  H.nav_ []
    [ H.a_ [ href_ (Widget (Capture 10) (Path "foo") (Capture "bar") (QueryParam (Just 0))) ]
        [ "Open widget" ]
    , H.a_ [ href_ About ] [ "About" ]
    ]

Reacting to URL changes

Use one of the two subscriptions from Miso.Subscription.History to react to navigation events:

uriSub — raw URI

uriSub :: (URI -> action) -> Sub action
Fires every time the browser popstate event fires (i.e. after pushURI, replaceURI, back, forward, or go), delivering the current URI.

routerSub — parsed route

routerSub :: Router route => (Either RoutingError route -> action) -> Sub action
Built on top of uriSub. Parses the raw URI through your Router instance before delivering the action.
data Action
  = HandleRoute (Either RoutingError Route)
  | ...

app :: App model Action
app = (component initialModel update view)
  { subs = [ routerSub HandleRoute ]
  }

Programmatic navigation

Push or replace entries on the history stack from within an Effect:
import Miso.Subscription.History (pushURI, replaceURI, back, forward, go)

update :: Action -> Effect parent model Action
update = \case
  GoToAbout ->
    io_ $ pushURI (toURI About)
  GoBack ->
    io_ back
pushURI and replaceURI both fire a synthetic popstate event after updating the history stack, so any active uriSub or routerSub will receive the new URL immediately.

Full working example

{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DeriveGeneric      #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase         #-}

module Main where

import Miso
import Miso.Router
import Miso.Subscription.History
import qualified Miso.Html.Element   as H
import qualified Miso.Html.Property  as HP

-- Route definition
data Route
  = Home
  | About
  | Widget (Capture "id" Int)
  deriving stock (Generic, Show, Eq)
  deriving anyclass Router

-- Model and Action
data Model = Model { currentRoute :: Either RoutingError Route }
data Action = RouteChanged (Either RoutingError Route)

-- Update
update :: Action -> Effect parent Model Action
update = \case
  RouteChanged r -> modify $ \m -> m { currentRoute = r }

-- View
view :: Model -> View Model Action
view Model {..} =
  H.div_ []
    [ H.nav_ []
        [ H.a_ [ href_ Home  ] [ "Home"  ]
        , H.a_ [ href_ About ] [ "About" ]
        ]
    , case currentRoute of
        Right Home             -> H.p_ [] [ "Welcome home" ]
        Right About            -> H.p_ [] [ "About page" ]
        Right (Widget (Capture n)) -> H.p_ [] [ text ("Widget " <> ms n) ]
        Left  _                -> H.p_ [] [ "Not found" ]
    ]

-- Entry point
main :: IO ()
main = miso defaultEvents $ \uri ->
  (component (Model (route uri)) update view)
    { subs = [ routerSub RouteChanged ] }

Build docs developers (and LLMs) love