diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for servant-coherent
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2024, Isaac Shapira
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Isaac Shapira nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/servant-lint.cabal b/servant-lint.cabal
new file mode 100644
--- /dev/null
+++ b/servant-lint.cabal
@@ -0,0 +1,51 @@
+cabal-version:   3.0
+name:            servant-lint
+version:         0.1.0.0
+synopsis:        Lint Servant Routes
+description:     Lint Servant Routes and reject bad routes, overlaps, and more
+license:         BSD-3-Clause
+license-file:    LICENSE
+author:          Isaac Shapira
+maintainer:      isaac.shapira@platonic.systems
+category:        Web
+build-type:      Simple
+extra-doc-files: CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://gitlab.com/platonic/servant-lint.git
+
+common warnings
+  ghc-options: -Wall
+
+library
+  import:           warnings
+  exposed-modules:  Servant.Lint
+  build-depends:
+    base >= 4.18.2 && < 4.19,
+    bytestring >= 0.11.5 && < 0.12,
+    containers >= 0.6.7 && < 0.7,
+    safe-coloured-text >= 0.2.0 && < 0.3,
+    text >= 2.0.2 && < 2.1,
+    servant >= 0.20.1 && < 0.21,
+    servant-server >= 0.20 && < 0.21
+  hs-source-dirs:   src
+  default-language: GHC2021
+
+test-suite servant-lint-test
+  import:           warnings
+  default-language: GHC2021
+  ghc-options:      -threaded -rtsopts -with-rtsopts=-N
+  other-modules: Servant.Lint
+  type:             exitcode-stdio-1.0
+  hs-source-dirs:   test src
+  main-is:          Main.hs
+  build-depends:
+    , base
+    , bytestring
+    , containers
+    , safe-coloured-text
+    , servant
+    , servant-server
+    , sydtest
+    , text
diff --git a/src/Servant/Lint.hs b/src/Servant/Lint.hs
new file mode 100644
--- /dev/null
+++ b/src/Servant/Lint.hs
@@ -0,0 +1,230 @@
+{-# LANGUAGE AllowAmbiguousTypes  #-}
+{-# LANGUAGE DataKinds            #-}
+{-# LANGUAGE DerivingStrategies   #-}
+{-# LANGUAGE LambdaCase           #-}
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE TypeFamilies         #-}
+{-# LANGUAGE UndecidableInstances #-}
+module Servant.Lint
+  ( lintAPI
+  , lintAPI'
+  , printLintAPI
+  , Error(..)
+  , Path(..)
+  , Lintable(..)
+  , Ambiguity(..)
+  , unlinesChunks
+  ) where
+
+import           Data.ByteString           (ByteString)
+import           Data.Containers.ListUtils (nubOrd)
+import           Data.Kind                 (Constraint, Type)
+import           Data.Maybe                (mapMaybe, maybeToList)
+import           Data.Proxy                (Proxy (Proxy))
+import qualified Data.Text                 as Text (pack, unpack)
+import           Data.Text.Encoding        (decodeUtf8)
+import           Data.Typeable             (TypeRep, Typeable, typeRep)
+import           GHC.Generics              (Generic)
+import           GHC.TypeLits              (KnownNat, KnownSymbol, natVal,
+                                            symbolVal)
+import           Servant.API               (Capture', CaptureAll, NoContent,
+                                            NoContentVerb, QueryParam,
+                                            QueryParams, ReflectMethod (..),
+                                            ReqBody', Verb, type (:<|>),
+                                            type (:>))
+import           Text.Colour               (Chunk, TerminalCapabilities (..),
+                                            bold, renderChunksText)
+import           Text.Colour.Chunk         (chunk, fore, red)
+
+-- | A term level representation of the API
+-- Its defined recursive to flatten the API to a list of routes instead of a tree
+type Path :: Type
+data Path
+  = PPath String !Path
+  | PCapture String TypeRep !Path
+  | PCaptureAll String TypeRep !Path
+  | PQueryParam String TypeRep !Path
+  | PQueryParams String TypeRep !Path
+  | PReqBody TypeRep !Path
+  | PVerb ByteString Integer TypeRep
+  deriving (Eq, Ord)
+
+instance Show Path where
+  show (PPath s p)           = "\"" <> s <> "\" :> " <> show p
+  show (PCapture s t p)      = "Capture \"" <> s <> "\" " <> show t <> " :> " <> show p
+  show (PCaptureAll s t p)   = "CaptureAll \"" <> s <> "\" " <> show t <> " :> " <> show p
+  show (PQueryParam s t p)   = "QueryParam \"" <> s <> "\" " <> show t <> " :> " <> show p
+  show (PQueryParams s t p)  = "QueryParams \"" <> s <> "\" " <> show t <> " :> " <> show p
+  show (PReqBody t p)        = "ReqBody _ _ " <> show t <> " :> " <> show p
+  show (PVerb method code t) = "Verb '" <> Text.unpack (decodeUtf8 method) <> " " <> show code <> " " <> show t
+
+-- | The Lintable type class describes how to go from a Servant Combinator to a @Path@
+-- This is essentially a function from `Type -> [Path]`
+-- If you have custom Servant Combinators you may need to add an instance of Lintable for your Combinator, typically ignoring the custom Combinator.
+type Lintable :: Type -> Constraint
+class Lintable a where
+  paths :: [Path]
+
+instance (Lintable a, Lintable b) => Lintable (a :<|> b) where
+  paths = paths @a <> paths @b
+
+instance (Lintable b, KnownSymbol hint, Typeable a) => Lintable (Capture' _mods hint a :> b) where
+  paths = PCapture (symbolVal (Proxy @hint)) (typeRep (Proxy @a))<$> paths @b
+
+instance (Lintable b, KnownSymbol hint, Typeable a) => Lintable (CaptureAll hint a :> b) where
+  paths = PCaptureAll (symbolVal (Proxy @hint)) (typeRep (Proxy @a)) <$> paths @b
+
+instance (Lintable b, KnownSymbol hint, Typeable a) => Lintable (QueryParam hint a :> b) where
+  paths = PQueryParam (symbolVal (Proxy @hint)) (typeRep (Proxy @a)) <$> paths @b
+
+instance (Lintable b, KnownSymbol hint, Typeable a) => Lintable (QueryParams hint a :> b) where
+  paths = PQueryParam (symbolVal (Proxy @hint)) (typeRep (Proxy @a)) <$> paths @b
+
+instance (Lintable b, Typeable a) => Lintable (ReqBody' _mods _ms a :> b) where
+  paths = PReqBody (typeRep (Proxy @a)) <$> paths @b
+
+instance (KnownSymbol a, Lintable b) => Lintable (a :> b) where
+  paths = PPath (symbolVal (Proxy @a)) <$> paths @b
+
+instance {-# OVERLAPPABLE #-}
+  ( ReflectMethod method
+  ) => Lintable (NoContentVerb method) where
+  paths = pure $ PVerb (reflectMethod (Proxy @method)) 204 (typeRep (Proxy @NoContent))
+
+instance {-# OVERLAPPABLE #-}
+  ( Typeable ret
+  , ReflectMethod method
+  , KnownNat code
+  ) => Lintable (Verb method code _cs ret) where
+  paths = pure $ PVerb (reflectMethod (Proxy @method)) (natVal (Proxy @code)) (typeRep (Proxy @ret))
+
+-- | This is a striped down version of the @Path@ focusing on removing details that ambiguate routes
+type Ambiguity :: Type
+data Ambiguity
+  = ACapture
+  | ACaptureAll
+  | AQueryParam String
+  | APath String
+  | AReqBody
+  | AVerb ByteString Integer
+  deriving (Eq, Ord, Show, Generic)
+
+-- | Non lawful Eq check
+(=!=) :: Ambiguity -> Ambiguity -> Bool
+ACapture =!= _                   = True
+_ =!= ACapture                   = True
+AReqBody =!= AReqBody            = True
+APath s =!= APath s'             = s == s'
+AVerb m c =!= AVerb m' c'        = m == m' && c == c'
+AQueryParam s =!= AQueryParam s' = s == s'
+_ =!= _                          = False
+
+-- | Non lawful Eq check
+(=!!=) :: [Ambiguity] -> [Ambiguity] -> Bool
+a@(ACaptureAll : _) =!!= b@(_:_) = last a =!= last b
+a@(_:_) =!!= b@(ACaptureAll:_)   = last a =!= last b
+(a:as) =!!= (b:bs)               = a =!= b && as =!!= bs
+[] =!!= []                       = True
+_ =!!= _                         = False
+
+ambiguity :: Path -> [Ambiguity]
+ambiguity = \case
+  PPath s p -> APath s : ambiguity p
+  PCapture _ _ p -> ACapture : ambiguity p
+  PCaptureAll _ _ p -> ACaptureAll : ambiguity p
+  PReqBody _ p -> AReqBody : ambiguity p
+  PQueryParam s _ p -> AQueryParam s : ambiguity p
+  PQueryParams s _ p -> AQueryParam s : ambiguity p
+  PVerb method code _ty -> [AVerb method code]
+
+checkForDuplicates :: Path -> Maybe Error
+checkForDuplicates p =
+  if length (nubOrd noStatic) /= length noStatic
+  then Just $ Error [[ chunk "Duplicate found in route "
+                     , bold $ chunk $ Text.pack (show p) <> ":"
+                    ]]
+  else Nothing
+  where
+  noStatic = filter (\case APath _ -> False; _ -> True) $ ambiguity p
+
+-- | Pretty errors via @Text.Colour@
+newtype Error = Error { toChunks :: [[Chunk]] }
+  deriving newtype (Eq, Show, Semigroup, Monoid)
+
+elem' :: [Ambiguity] -> [[Ambiguity]] -> Bool
+elem' = any . (=!!=)
+
+-- | Pass your API type for lint errors as Chunks
+-- Chunks are colored terminal bits from @Text.Colour@ for making
+-- pretty errors.
+lintAPI' :: forall api. Lintable api => [Error]
+lintAPI' = case go psAll of [x,_] -> [x]; x -> x
+  where
+  psAll = paths @api
+  go [] = []
+  go (p:ps) =
+    let ambiguities = [printAmbiguity psAll p | ambiguity p `elem'` (ambiguity <$> deleteFirst p psAll)]
+        badReturns  = mapMaybe (badReturn psAll p) psAll
+        duplicates  = checkForDuplicates p
+    in ambiguities <> badReturns <> maybeToList duplicates <> go ps
+
+-- | Pass your API type for lint errors thrown in IO
+-- This is typically useful for testing
+lintAPI :: forall api. Lintable api => IO ()
+lintAPI = case lintAPI' @api of
+    [] -> pure ()
+    ls -> error $ Text.unpack $ renderChunksText With24BitColours . unlinesChunks $ unlinesChunks . toChunks <$> ls
+
+-- | Pass your API type for lint via @putStrLn@ in stdout
+printLintAPI :: forall api. Lintable api => IO ()
+printLintAPI = case lintAPI' @api of
+    [] -> pure ()
+    ls -> putStrLn $ Text.unpack $ renderChunksText With24BitColours . unlinesChunks $ unlinesChunks . toChunks <$> ls
+
+deleteFirst :: Eq t => t -> [t] -> [t]
+deleteFirst _ [] = []
+deleteFirst a (b:bc) | a == b    = bc
+                     | otherwise = b : deleteFirst a bc
+
+badReturn :: [Path] -> Path -> Path -> Maybe Error
+badReturn psAll c = \case
+  PPath _ p -> badReturn psAll c p
+  PCapture _ _ p -> badReturn psAll c p
+  PCaptureAll _ _ p -> badReturn psAll c p
+  PQueryParam _ _ p -> badReturn psAll c p
+  PVerb _method 500 _ty -> Just $ Error $
+     [ chunk "Bad verb, you should never intentionally return 500 as part of your API:"
+     ] : (badReturnColor <$> psAll)
+  PVerb _method code ty | code /= 204 && ty == typeRep (Proxy @NoContent) -> Just $ Error $
+     [ chunk "Bad verb, NoContent must use HTTP Status Code 204, not "
+     , bold $ chunk $ Text.pack $ show code <> ":"
+     ] : (badReturnColor <$> psAll)
+  PReqBody _ (PVerb "GET" _ _) -> Just $ Error $
+     [ chunk "Bad verb, do not use ReqBody in a GET request, Http 1.1 says its meaningless"
+     ] : (badReturnColor <$> psAll)
+  PReqBody _ (PVerb {}) -> Nothing
+  PReqBody _ _ -> Just $ Error $
+     [ chunk "ReqBody must be the last combinator before the Verb"
+     ] : (badReturnColor <$> psAll)
+  _ -> Nothing
+  where
+    badReturnColor :: Path -> [Chunk]
+    badReturnColor p' = pure $
+      if c == p'
+      then fore red $ chunk $ ("\t" <>) $ Text.pack $ show p' <> " 👈"
+      else chunk $ ("\t" <>) $ Text.pack $ show p'
+
+printAmbiguity :: [Path] -> Path -> Error
+printAmbiguity ps p = Error $
+    [ chunk "Ambiguous with "
+    , bold $ chunk $ Text.pack (show p) <> ":"
+    ] : (overlappingColor <$> ps)
+  where
+    overlappingColor p' = pure $
+      if ambiguity p =!!= ambiguity p'
+      then fore red $ chunk $ ("\t" <>) $ Text.pack $ show p' <> " 👈"
+      else chunk $ ("\t" <>) $ Text.pack $ show p'
+
+-- | Exported for testing only
+unlinesChunks :: [[Chunk]] -> [Chunk]
+unlinesChunks = concatMap (<> [chunk "\n"])
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,222 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE ExplicitNamespaces  #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# OPTIONS_GHC -Wno-unused-imports #-}
+{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
+{-# LANGUAGE DerivingVia         #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+module Main (main) where
+
+import           Data.ByteString (ByteString)
+import           Data.String     (fromString)
+import           Data.Text       as Text (unpack)
+import           Servant         (Capture, CaptureAll, Get, JSON, NoContent,
+                                  NoContentVerb, Post, QueryParam, QueryParams,
+                                  ReqBody, StdMethod (GET, POST), Verb,
+                                  type (:<|>), type (:>))
+import           Servant.Lint    (Ambiguity, Error (..), Lintable, lintAPI',
+                                  unlinesChunks)
+import           Test.Syd        (describe, expectationFailure, it, shouldBe,
+                                  sydTest, xdescribe)
+import           Text.Colour     (Chunk,
+                                  TerminalCapabilities (With24BitColours), bold,
+                                  chunk, fore, red, renderChunksText)
+
+type DisambiguatedQuery
+     = QueryParam "foo" Int :> Get '[JSON] ()
+  :<|> QueryParam "bar" Int :> Get '[JSON] ()
+
+type DisambiguatedMethod
+     = Post '[JSON] ()
+  :<|> Get '[JSON] ()
+
+type DisambiguatedReturn
+     = Get '[JSON] Bool
+  :<|> Get '[JSON] ()
+
+type DisambiguatedStatic
+     = "foo" :> Get '[JSON] ()
+  :<|> "bar" :> Get '[JSON] ()
+
+type DisambiguatedNoContent
+     = "foo" :> NoContentVerb 'GET
+
+type DisambiguatedReqBody
+     = ReqBody '[JSON] () :> Post '[JSON] ()
+  :<|> "bar" :> ReqBody '[JSON] () :> Post '[JSON] ()
+
+type AmbiguousRoot
+     = Get '[JSON] ()
+  :<|> Get '[JSON] ()
+
+type AmbiguousStatic
+     = "foo" :> Get '[JSON] ()
+  :<|> "foo" :> Get '[JSON] ()
+
+type AmbiguousCapture
+     = "bar" :> Capture "gurf" Int :> Get '[JSON] ()
+  :<|> "foo" :> Get '[JSON] ()
+  :<|> "bar" :> Capture "wat" Int :> Get '[JSON] ()
+
+type AmbiguousCaptureWithStatic
+     = "bar" :> Capture "gurf" Int :> Get '[JSON] ()
+  :<|> "bar" :> "5" :> Get '[JSON] ()
+  :<|> "bar" :> "5" :> "wat" :> Get '[JSON] ()
+
+type AmbiguousCaptureAllWithStatic
+     = "bar" :> CaptureAll "murf"  [Int] :> Get '[JSON] ()
+  :<|> "bar" :> "5" :> Get '[JSON] ()
+  :<|> "bar" :> Capture "how" Int :> Get '[JSON] ()
+  :<|> "bar" :> "3" :> "2" :> Get '[JSON] ()
+
+type AmbiguousQuery
+     = QueryParam "foo" Int :> Get '[JSON] ()
+  :<|> QueryParam "foo" String :> Get '[JSON] ()
+
+type AmbiguousQuerys
+     = QueryParams "foo" Int :> Get '[JSON] ()
+  :<|> QueryParams"foo" String :> Get '[JSON] ()
+
+type AmbiguousReqBody
+     = ReqBody '[JSON] Int :> Post '[JSON] ()
+  :<|> ReqBody '[JSON] String :> Post '[JSON] ()
+
+type Bad200NoContent
+     = "foo" :> Get '[JSON] NoContent
+  :<|> "bar" :> Get '[JSON] ()
+
+type BadReqBodyGet
+    = "foo" :> ReqBody '[JSON] () :> Get '[JSON] ()
+
+type BadReqBodyPosition
+    = ReqBody '[JSON] () :> "bar" :> Post '[JSON] ()
+
+type Bad500Code
+  = "foo" :> Verb 'POST 500 '[JSON] ()
+
+type Duplicates
+  = "bar" :> CaptureAll "foo" () :> "baz" :> CaptureAll "bar" () :> Get '[JSON] ()
+  :<|> "hunk" :> QueryParam "foo" () :> QueryParam "foo" Bool :> Get '[JSON] ()
+  :<|> "baz" :> Capture "foo" () :> Capture "foo" Bool :> Get '[JSON] ()
+  :<|> "foo" :> "foo" :> "foo" :> Get '[JSON] ()
+
+lintShouldBe :: forall api. Lintable api => [[Chunk]] -> IO ()
+lintShouldBe expected' = let
+    renderError = renderChunksText With24BitColours . unlinesChunks . fmap (unlinesChunks . toChunks)
+    actual = renderError $ lintAPI' @api
+    expected = renderError [Error expected']
+  in if expected' == mempty || actual == expected then pure ()
+     else expectationFailure $ Text.unpack $ "Expected:\n\n" <> expected <> "But Got:\n\n" <> actual
+
+main :: IO ()
+main = sydTest $ do
+
+  describe "Disambiguation" $ do
+    it "DisambiguatedQuery" $ lintShouldBe @DisambiguatedQuery $ mempty
+    it "DisambiguatedMethod" $ lintShouldBe @DisambiguatedMethod $ mempty
+    it "DisambiguatedReturn" $ lintShouldBe @DisambiguatedReturn $ mempty
+    it "DisambiguatedStatic" $ lintShouldBe @DisambiguatedStatic $ mempty
+    it "DisambiguatedNoContent" $ lintShouldBe @DisambiguatedNoContent $ mempty
+    it "DisambiguatedReqBody" $ lintShouldBe @DisambiguatedReqBody $ mempty
+
+  describe "Ambiguation" $ do
+    it "AmbiguousRoot" $ lintShouldBe @AmbiguousRoot
+        [ [ chunk "Ambiguous with ", bold $ chunk "Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\tVerb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\tVerb 'GET 200 () 👈"
+        ]
+    it "AmbiguousCapture" $ lintShouldBe @AmbiguousCapture
+        [ [ chunk "Ambiguous with ", bold $ chunk "\"bar\" :> Capture \"gurf\" Int :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"bar\" :> Capture \"gurf\" Int :> Verb 'GET 200 () 👈"
+        , pure $ chunk "\t\"foo\" :> Verb 'GET 200 ()"
+        , pure $ fore red $ chunk "\t\"bar\" :> Capture \"wat\" Int :> Verb 'GET 200 () 👈"
+        ]
+    it "AmbiguousCaptureWithStatic" $ lintShouldBe @AmbiguousCaptureWithStatic
+        [ [ chunk "Ambiguous with ", bold $ chunk "\"bar\" :> Capture \"gurf\" Int :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"bar\" :> Capture \"gurf\" Int :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> \"5\" :> Verb 'GET 200 () 👈"
+        , pure $ chunk "\t\"bar\" :> \"5\" :> \"wat\" :> Verb 'GET 200 ()"
+        ]
+    it "AmbiguousCaptureAllWithStatic" $ lintShouldBe @AmbiguousCaptureAllWithStatic
+        [ [ chunk "Ambiguous with ", bold $ chunk "\"bar\" :> CaptureAll \"murf\" [Int] :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"bar\" :> CaptureAll \"murf\" [Int] :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> \"5\" :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> Capture \"how\" Int :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> \"3\" :> \"2\" :> Verb 'GET 200 () 👈"
+        , []
+        , [ chunk "Ambiguous with ", bold $ chunk "\"bar\" :> \"5\" :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"bar\" :> CaptureAll \"murf\" [Int] :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> \"5\" :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> Capture \"how\" Int :> Verb 'GET 200 () 👈"
+        , pure $ chunk "\t\"bar\" :> \"3\" :> \"2\" :> Verb 'GET 200 ()"
+        , []
+        , [ chunk "Ambiguous with ", bold $ chunk "\"bar\" :> Capture \"how\" Int :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"bar\" :> CaptureAll \"murf\" [Int] :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> \"5\" :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"bar\" :> Capture \"how\" Int :> Verb 'GET 200 () 👈"
+        , pure $ chunk "\t\"bar\" :> \"3\" :> \"2\" :> Verb 'GET 200 ()"
+        , []
+        , [ chunk "Ambiguous with ", bold $ chunk "\"bar\" :> \"3\" :> \"2\" :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"bar\" :> CaptureAll \"murf\" [Int] :> Verb 'GET 200 () 👈"
+        , pure $ chunk "\t\"bar\" :> \"5\" :> Verb 'GET 200 ()"
+        , pure $ chunk "\t\"bar\" :> Capture \"how\" Int :> Verb 'GET 200 ()"
+        , pure $ fore red $ chunk "\t\"bar\" :> \"3\" :> \"2\" :> Verb 'GET 200 () 👈"
+        ]
+    it "AmbiguousQuery" $ lintShouldBe @AmbiguousQuery
+        [ [ chunk "Ambiguous with ", bold $ chunk "QueryParam \"foo\" Int :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\tQueryParam \"foo\" Int :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\tQueryParam \"foo\" [Char] :> Verb 'GET 200 () 👈"
+        ]
+    it "AmbiguousQuerys" $ lintShouldBe @AmbiguousQuerys
+        [ [ chunk "Ambiguous with ", bold $ chunk "QueryParam \"foo\" Int :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\tQueryParam \"foo\" Int :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\tQueryParam \"foo\" [Char] :> Verb 'GET 200 () 👈"
+        ]
+    it "AmbiguousStatic" $ lintShouldBe @AmbiguousStatic
+        [ [ chunk "Ambiguous with ", bold $ chunk "\"foo\" :> Verb 'GET 200 ():" ]
+        , pure $ fore red $ chunk "\t\"foo\" :> Verb 'GET 200 () 👈"
+        , pure $ fore red $ chunk "\t\"foo\" :> Verb 'GET 200 () 👈"
+        ]
+    it "AmbiguousReqBody" $ lintShouldBe @AmbiguousReqBody
+        [ [ chunk "Ambiguous with ", bold $ chunk "ReqBody _ _ Int :> Verb 'POST 200 ():" ]
+        , pure $ fore red $ chunk "\tReqBody _ _ Int :> Verb 'POST 200 () 👈"
+        , pure $ fore red $ chunk "\tReqBody _ _ [Char] :> Verb 'POST 200 () 👈"
+        ]
+
+  describe "Bad Returns" $ do
+    it "NoContent" $ lintShouldBe @Bad200NoContent
+      [ [ chunk "Bad verb, NoContent must use HTTP Status Code 204, not "
+      , bold $ chunk "200:" ]
+      , pure $ fore red $ chunk "\t\"foo\" :> Verb 'GET 200 NoContent 👈"
+      , pure $ chunk "\t\"bar\" :> Verb 'GET 200 ()"
+      ]
+
+    it "ReqBody on GET" $ lintShouldBe @BadReqBodyGet
+      [ [ chunk "Bad verb, do not use ReqBody in a GET request, Http 1.1 says its meaningless" ]
+      , pure $ fore red $ chunk "\t\"foo\" :> ReqBody _ _ () :> Verb 'GET 200 () 👈"
+      ]
+
+    it "500 not allowed as valid response code" $ lintShouldBe @Bad500Code
+      [ [ chunk "Bad verb, you should never intentionally return 500 as part of your API:" ]
+      , pure $ fore red $ chunk "\t\"foo\" :> Verb 'POST 500 () 👈"
+      ]
+
+    it "ReqBody not next to Verb" $ lintShouldBe @BadReqBodyPosition
+      [ [ chunk "ReqBody must be the last combinator before the Verb" ]
+      , pure $ fore red $ chunk "\tReqBody _ _ () :> \"bar\" :> Verb 'POST 200 () 👈"
+      ]
+
+  it "duplicates" $ lintShouldBe @Duplicates
+      [ [ chunk "Duplicate found in route "
+        , bold $ chunk "\"bar\" :> CaptureAll \"foo\" () :> \"baz\" :> CaptureAll \"bar\" () :> Verb 'GET 200 ():"
+        ]
+      , []
+      , [ chunk "Duplicate found in route "
+        , bold $ chunk "\"hunk\" :> QueryParam \"foo\" () :> QueryParam \"foo\" Bool :> Verb 'GET 200 ():"
+        ]
+      , []
+      , [ chunk "Duplicate found in route "
+        , bold $ chunk "\"baz\" :> Capture \"foo\" () :> Capture \"foo\" Bool :> Verb 'GET 200 ():"
+        ]
+      ]
