diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
-## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.0.1.0...main)
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/v1.0.2.0...main)
 
 - None.
+
+## [v1.0.2.0](https://github.com/freckle/freckle-app/compare/v1.0.1.0...v1.0.2.0)
+
+- Add 'Freckle.App.Yesod.Route' to allow printing route names.
 
 ## [v1.0.1.0](https://github.com/freckle/freckle-app/compare/v1.0.0.4...v1.0.1.0)
 
diff --git a/freckle-app.cabal b/freckle-app.cabal
--- a/freckle-app.cabal
+++ b/freckle-app.cabal
@@ -1,6 +1,6 @@
 cabal-version:      1.12
 name:               freckle-app
-version:            1.0.1.0
+version:            1.0.2.0
 license:            MIT
 license-file:       LICENSE
 maintainer:         Freckle Education
@@ -45,6 +45,7 @@
         Freckle.App.Version
         Freckle.App.Wai
         Freckle.App.Yesod
+        Freckle.App.Yesod.Routes
         Network.HTTP.Link.Compat
 
     hs-source-dirs:     library
@@ -101,6 +102,7 @@
         resource-pool >=0.2.3.2,
         retry >=0.8.1.2,
         rio >=0.1.20.0,
+        template-haskell >=2.16.0.0,
         text >=1.2.4.1,
         time >=1.9.3,
         transformers >=0.5.6.2,
diff --git a/library/Freckle/App/Yesod/Routes.hs b/library/Freckle/App/Yesod/Routes.hs
new file mode 100644
--- /dev/null
+++ b/library/Freckle/App/Yesod/Routes.hs
@@ -0,0 +1,64 @@
+module Freckle.App.Yesod.Routes
+  ( mkRouteNameCaseExp
+  ) where
+
+import Prelude
+
+import Data.Foldable (fold)
+import qualified Language.Haskell.TH as TH
+import Yesod.Routes.TH.Types
+
+-- | Lambdacase expression to print route names
+--
+-- It has the following type:
+--
+-- > _ :: Route a -> String
+--
+-- It produces code like:
+--
+-- > \case
+-- >   RoutePiece a -> case a of
+-- >     RouteResource{} -> "ResourceName"
+--
+mkRouteNameCaseExp :: [ResourceTree String] -> TH.Q TH.Exp
+mkRouteNameCaseExp tree = TH.LamCaseE . fold <$> traverse mkMatches tree
+
+-- | Make match expressions for a big case over routes
+--
+-- > RoutePiece a -> case a of
+-- >   ...
+--
+mkMatches :: ResourceTree String -> TH.Q [TH.Match]
+mkMatches (ResourceLeaf resource) = pure [mkLeafMatch resource]
+mkMatches (ResourceParent name _checkOverlap params children) = do
+  caseVar <- TH.newName "a"
+  let
+    -- by convention the final param in a route is the next route constructor
+    paramVars =
+      fmap (const TH.WildP) (filter isDynamic params) <> [TH.VarP caseVar]
+  matches <- fold <$> traverse mkMatches children
+  pure
+    [ TH.Match
+        (TH.ConP constName paramVars)
+        (TH.NormalB $ TH.CaseE (TH.VarE caseVar) matches)
+        []
+    ]
+  where constName = TH.mkName name
+
+isDynamic :: Piece a -> Bool
+isDynamic = \case
+  Static{} -> False
+  Dynamic{} -> True
+
+-- | Leaf match expressions for a resource
+--
+-- > Name{} -> "ResourceName"
+--
+mkLeafMatch :: Resource String -> TH.Match
+mkLeafMatch resource = TH.Match
+  (TH.RecP constName [])
+  (TH.NormalB $ TH.LitE $ TH.StringL name)
+  []
+ where
+  constName = TH.mkName name
+  name = resourceName resource
diff --git a/package.yaml b/package.yaml
--- a/package.yaml
+++ b/package.yaml
@@ -1,6 +1,6 @@
 ---
 name: freckle-app
-version: 1.0.1.0
+version: 1.0.2.0
 maintainer: Freckle Education
 category: Utils
 github: freckle/freckle-app
@@ -90,6 +90,7 @@
     - resource-pool
     - retry
     - rio
+    - template-haskell
     - text
     - time
     - transformers
