yesod-core-1.7.0.0: docs/split-route-compilation.md
# Split route compilation
As of yesod-core 1.7, route definitions can be split across multiple modules.
A nested route block gets its own datatype, instances, and dispatch generated
in its own module, and the main `mkYesod` splice *delegates* to that module
instead of regenerating everything in one place.
Why you'd want this:
* **Smaller TH splices.** A large site no longer needs one giant `mkYesod`
splice that regenerates everything whenever any route changes.
* **Parallel compilation.** Each fragment module compiles independently.
* **Locality.** A route group's handlers, datatype, and dispatch live in one
module, next to each other.
* **Faster test feedback.** Each fragment gets its own `YesodDispatchNested`
instance, which is enough to dispatch a request on its own.
[hspec-yesod](https://github.com/parsonsmatt/hspec-yesod) is building on
this to run specs against a single route fragment, so a spec depends only on
the handlers it actually exercises — editing an unrelated handler no longer
recompiles (or relinks) the test module.
## How it works
Any nested route block — a parent declared with a trailing `:` — gets its own
route datatype:
```
/nest NestR:
/ NestIndexR GET POST
/#Int NestShowR GET POST
```
By default, `mkYesod` generates the `NestR` datatype and its dispatch inline,
exactly as in 1.6. But if a `YesodDispatchNested NestR` instance is already in
scope at the splice site (because a separately compiled module generated it),
`mkYesod` delegates to that instance instead. Single-module sites are
unchanged; splitting is opt-in and per-parent.
## Recipe: top-level site
Three modules: a shared route table, the split-out fragment, and the main site.
First, put the route definitions in their own module so both sides can see
them — along with your project's `RouteOpts`. Every splice that touches the
route table should use the same options, so define them once (see
[Fallthrough](#fallthrough) for why fallthrough should be on):
```haskell
module App.Routes.Resources where
import Yesod.Core
appRouteOpts :: RouteOpts
appRouteOpts = setNestedRouteFallthrough True defaultOpts
appRouteOptsFor :: String -> RouteOpts
appRouteOptsFor name = setFocusOnNestedRoute name appRouteOpts
appResources :: [ResourceTree String]
appResources = [parseRoutes|
/ HomeR GET
/nest NestR:
/ NestIndexR GET POST
/#Int NestShowR GET POST
|]
```
Then generate the `NestR` fragment in its own module by focusing the splice
on it. The fragment's handlers live here too:
```haskell
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module App.Routes.NestR where
import Yesod.Core
import Data.Text (Text)
import App.Routes.Resources
mkYesodOpts (appRouteOptsFor "NestR") "App" appResources
getNestIndexR :: HandlerFor App Text
getNestIndexR = pure "nest index"
postNestIndexR :: HandlerFor App Text
postNestIndexR = pure "posted"
getNestShowR :: Int -> HandlerFor App Text
getNestShowR _ = pure "nest show"
postNestShowR :: Int -> HandlerFor App Text
postNestShowR _ = pure "posted"
```
Finally, the main module imports the fragment and splices with the same
options:
```haskell
module App where
import Yesod.Core
import App.Routes.Resources
import App.Routes.NestR (NestR (..))
mkYesodOpts appRouteOpts "App" appResources
getHomeR :: HandlerFor App Text
getHomeR = pure "home"
```
Because `App.Routes.NestR` compiled first, the `mkYesodOpts` splice sees the
`YesodDispatchNested NestR` instance and delegates to it rather than
regenerating the `NestR` code. If you want to separate the datatype from the
dispatch further, `mkYesodDataOpts` and `mkYesodDispatchOpts` accept the same
options.
## Recipe: subsites
Subsites split the same way. The data module generates the subsite's route
datatype and nested-fragment instances:
```haskell
module App.SplitSub.Data where
import Yesod.Core
data SplitSub = SplitSub
mkYesodSubData "SplitSub" [parseRoutes|
/ SplitHomeR GET
/nested NestedR:
/ NestedHomeR GET
/detail/#Int NestedDetailR GET
|]
```
The split-out module holds the nested handlers and generates the
`YesodSubDispatchNested` instance with `mkNestedSubDispatchInstance`:
```haskell
{-# OPTIONS_GHC -Wno-orphans #-}
module App.SplitSub.NestedR (NestedR (..)) where
import Yesod.Core
import Yesod.Core.Dispatch
(mkNestedSubDispatchInstance, defaultOpts, TyArgs (..))
import App.SplitSub.Data
getNestedHomeR :: SubHandlerFor SplitSub master Text
getNestedHomeR = pure "nested home"
getNestedDetailR :: Int -> SubHandlerFor SplitSub master Text
getNestedDetailR _ = pure "nested detail"
$(mkNestedSubDispatchInstance
defaultOpts -- or your project-wide RouteOpts; see Fallthrough below
"NestedR"
[] -- no instance context
NoTyArgs -- no type arguments (non-parameterized subsite)
return -- handler unwrapper
resourcesSplitSub) -- the [ResourceTree String] from your routes file
```
`mkNestedSubDispatchInstance` takes the resources as `[ResourceTree String]` —
exactly what the `parseRoutes` quasi-quoter produces — and parses the type
strings internally, failing the splice with an attributable error on a
malformed type rather than a deferred runtime `error`.
And the subsite's `YesodSubDispatch` instance delegates automatically, as long
as the split-out instance is in scope:
```haskell
import App.SplitSub.Data
import App.SplitSub.NestedR () -- instance import only; no handlers leak
instance YesodSubDispatch SplitSub master where
yesodSubDispatch = $(mkYesodSubDispatch resourcesSplitSub)
```
For a subsite defined entirely in one module (including parameterized subsites
like `data MySub a`), `mkYesodSubDispatchInstance "(MyClass a) => MySub a"
resourcesMySub` generates the `YesodSubDispatch` and nested instances in one
splice.
## Linking to nested routes
A nested fragment constructor isn't a `Route App` on its own — its parent may
bind dynamic pieces the fragment doesn't carry. To use a fragment in
`redirect` or `setUrl`, wrap it in `WithParentArgs` together with the parent's
dynamic arguments:
```haskell
redirect (WithParentArgs userId (ProfileEditR "name"))
```
When the parent binds no dynamic pieces, the bare constructor works directly —
generated `RedirectUrl`/`UrlToDispatch` instances fill in the empty parent
arguments:
```haskell
redirect NestIndexR
```
For this reason, if you are splitting up your routes for compilation, it is
recommended to avoid captures in the parent prefix (prefer
`/admin AdminR:` over `/user/#UserId UserR:`). With a static-only prefix,
every fragment constructor is usable directly in `redirect`/`setUrl`, and
`WithParentArgs` never enters the picture. Dynamic pieces can still live on
the individual child routes.
To convert a fragment into its parent's route type explicitly, use
`toParentRoute` from `Yesod.Core.Class.Dispatch.ToParentRoute` (not
re-exported from `Yesod.Core`, since the name is easy to collide with).
## Fallthrough
By default, once dispatch enters a nested parent whose subtree has no matching
child, the response is a 404 — later sibling routes are never tried. This
matters a lot when splitting: the usual first step is wrapping a batch of
previously-flat routes in a new nested group, and with fallthrough off, any
request matching the group's prefix now *commits* to that group. A route
declared later that shares the prefix silently becomes unreachable — flat
dispatch would have kept trying, the grouped version 404s. Enabling
`setNestedRouteFallthrough` restores the flat-dispatch behavior: a parent
whose subtree has no match falls through to the routes after it.
Fallthrough is decided per splice: each module containing a parent route
decides for its own parents. Mixing modules spliced with different options
gives confusingly inconsistent dispatch, which is why the recipe above defines
`appRouteOpts`/`appRouteOptsFor` once and uses them everywhere — don't reach
for `defaultOpts` directly in individual modules.
Related gotcha: a nested parent with *no* leading static path piece matches
unconditionally, so siblings declared after it are unreachable unless
fallthrough is enabled. Give the parent a static segment or enable
fallthrough.
## Troubleshooting
* **`Target 'NestR' was not found in resources.`** — the name passed to
`setFocusOnNestedRoute` doesn't match any nested parent in the route table.
* **Missing-extension errors** — fragment-generating modules need
`MultiParamTypeClasses`, and usually `FlexibleContexts` and
`FlexibleInstances`. GHC's error names the missing extension.
* **Duplicate `RedirectUrl` instance** — the generated convenience instance is
`OVERLAPPABLE`, so a *more specific* hand-written instance wins, but a
hand-written instance with the identical head is still a duplicate-instance
error.