diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,14 @@
+# Changelog for webgear-server
+
+## Unreleased changes
+
+## [0.1.0] - 2020-08-16
+
+### Added
+
+- Support basic traits and middlewares
+- Automated tests
+- Documentation
+
+[0.1.0]: https://github.com/rkaippully/webgear/compare/0.0.0...0.1.0
+[Unreleased]: https://github.com/rkaippully/webgear/compare/0.1.0...HEAD
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+# WebGear - HTTP API server
+
+[![Hackage](https://img.shields.io/hackage/v/webgear-server)](https://hackage.haskell.org/package/webgear-server)
+[![Build Status](https://img.shields.io/github/workflow/status/rkaippully/webgear/Haskell%20CI/master)](https://github.com/rkaippully/webgear/actions?query=workflow%3A%22Haskell+CI%22+branch%3Amaster)
+
+WebGear is a Haskell library for building composable, type-safe HTTP API servers. It focuses on good documentation and
+usability.
+
+See the documentation of WebGear module to get started.
diff --git a/src/WebGear.hs b/src/WebGear.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear.hs
@@ -0,0 +1,243 @@
+{-# OPTIONS_GHC -Wno-unused-imports #-}
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- WebGear helps to build composable, type-safe HTTP API servers.
+--
+-- The documentation below gives an overview of WebGear. Example
+-- programs built using WebGear are available at
+-- https://github.com/rkaippully/webgear/tree/master/webgear-examples.
+--
+module WebGear
+  ( -- * Serving HTTP APIs
+    -- $serving
+
+    -- * Traits and Linking
+    -- $traits
+
+    -- * Handlers
+    -- $handlers
+
+    -- * Middlewares
+    -- $middlewares
+
+    -- * Routing
+    -- $routing
+
+    -- * Running the Server
+    -- $running
+  ) where
+
+import Control.Applicative (Alternative (..))
+import Control.Arrow (Kleisli (..))
+import Web.HttpApiData (FromHttpApiData)
+
+import qualified Network.Wai as Wai
+
+import WebGear.Middlewares
+import WebGear.Route
+import WebGear.Trait
+import WebGear.Trait.Body
+import WebGear.Trait.Header
+import WebGear.Trait.Method
+import WebGear.Trait.Path
+import WebGear.Types
+
+
+--
+-- $serving
+--
+-- An HTTP API server handler can be thought of as a function that
+-- takes a request as input and produces a response as output in a
+-- monadic context.
+--
+-- @
+-- handler :: Monad m => 'Request' -> m 'Response'
+-- @
+--
+-- Most APIs will require extracting some information from the
+-- request, processing it and then producing a response. For example,
+-- the server might require access to some HTTP header values, query
+-- parameters, or the request body. WebGear allows to access such
+-- information using traits.
+--
+--
+-- $traits
+--
+-- A trait is an attribute associated with a value. For example, a
+-- @Request@ might have a header that we are interested in; the
+-- 'Header' trait represents that. All traits have instances of
+-- 'Trait' typeclass. This typeclass helps to 'check' the presence of
+-- the trait. It also has two associated types - 'Val' and 'Fail' - to
+-- represent the result of 'check'ing the presence of a trait.
+--
+-- For example, the 'Header' trait has an instance of the 'Trait'
+-- typeclass. The 'check' function evaluates to a 'CheckSuccess' value
+-- if the header exists and can be converted to an attribute via the
+-- 'FromHttpApiData' typeclass. Otherwise, it evaluates to a
+-- 'CheckFail' value.
+--
+-- WebGear provides type-safety by linking traits to the request or
+-- response at type level. The 'Linked' data type associates a
+-- 'Request' or 'Response' with a list of traits. This linking
+-- guarantees that the Request or Response has the specified trait.
+--
+-- These functions work with traits and linked values:
+--
+--   * 'linkzero': Establish a link between a value and an empty list
+--     of traits. This always succeeds.
+--   * 'linkplus': Attempts to establish a link between a linked value
+--     with an additional trait.
+--   * 'linkminus': Removes a trait from the list of linked traits.
+--   * 'unlink': Convert a linked value to a regular value without any
+--     type-level traits.
+--   * 'traitValue': Extract a 'Val' associated with a trait from a
+--     linked value.
+--
+-- For example, we make use of the @'Method' \@GET@ trait to ensure
+-- that our handler is called only for GET requests. We can link a
+-- request value with this trait using:
+--
+-- @
+-- linkedRequest :: Monad m => 'Request' -> m (Either 'MethodMismatch' ('Linked' '['Method' GET] 'Request'))
+-- linkedRequest = 'linkplus' @('Method' GET) . 'linkzero'
+-- @
+--
+-- Let us modify the type signature of our handler to use linked
+-- values instead of regular values:
+--
+-- > handler :: Monad m => Linked req Request -> m (Linked res Response)
+--
+-- Here, @req@ is a type-level list of traits associated with the
+-- @Request@ that this handler requires and @res@ is a type-level list
+-- of traits associated with the @Response@ that this handler will
+-- produce. This implies that this handler can be called only with a
+-- request possessing certain traits and it is guaranteed to produce a
+-- response having certain traits.
+--
+--
+-- $handlers
+--
+-- Handlers in WebGear are defined with a type very similar to the
+-- above.
+--
+-- @
+-- type 'Handler' m req res a = 'Kleisli' m ('Linked' req 'Request') ('Linked' res ('Response' a))
+-- @
+--
+-- It is a 'Kleisli' arrow as described in the above section with
+-- type-level trait lists. However, the response is parameterized by
+-- the type variable @a@, which represents the type of the response
+-- body.
+--
+-- A handler can extract some trait attribute of a request with the
+-- 'traitValue' function. It can also use 'linkplus' function to prove
+-- the presence of traits in the response before returning it.
+--
+--
+-- $middlewares
+--
+-- A middleware is a higher-order function that takes a handler as
+-- input and produces another handler with potentially different lists
+-- of request and response traits. Thus middlewares can augment the
+-- functionality of another handler.
+--
+-- For example, here is the definition of the 'method' middleware:
+--
+-- @
+-- method :: ('IsStdMethod' t, 'MonadRouter' m) => 'Handler' m ('Method' t:req) res a -> 'Handler' m req res a
+-- method handler = 'Kleisli' $ 'linkplus' \@('Method' t) >=> 'either' ('const' 'rejectRoute') ('runKleisli' handler)
+-- @
+--
+-- The @linkplus \@(Method t)@ function is used to prove the presence
+-- of the method @t@ in the request and the @handler@ is invoked only
+-- if the method matches. In case of a mismatch, this route is
+-- rejected by calling 'rejectRoute'.
+--
+-- Many middlewares can be composed to form complex request handling
+-- logic.
+--
+-- @
+-- putUser = 'method' \@PUT
+--           $ 'requestContentType' \@"application/json"
+--           $ 'jsonRequestBody' \@User
+--           $ 'jsonResponseBody' \@User
+--           $ putUserHandler
+-- @
+--
+--
+-- $routing
+--
+-- A typical server will have many routes and we would like to pick
+-- one based on the URL path, HTTP method etc. We need a couple of
+-- things to achieve this.
+--
+-- First, we need a way to indicate that a handler cannot handle a
+-- request, possibly because the path or method did not match with
+-- what was expected. This is achieved by the 'rejectRoute' function:
+--
+-- @
+-- class (Alternative m, MonadPlus m) => 'MonadRouter' m where
+--   'rejectRoute' :: m a
+--   'failHandler' :: 'Response' ByteString -> m a
+-- @
+--
+-- The 'failHandler' can be used in cases where we find a matching
+-- route but the request handling is aborted for some reason. For
+-- example, if a route requires the request Content-type header to
+-- have a particular value but the actual request had a different
+-- Content-type, 'failHandler' can be used to abort and return an
+-- error response.
+--
+-- Second, we need a mechanism to try an alternate route when one
+-- route is rejected. Since 'MonadRouter' is an 'Alternative', we can
+-- use '<|>' to combine many routes. When a request arrives, a match
+-- will be attempted against each route sequentially and the first
+-- matching route handler will process the request. Here is an
+-- example:
+--
+-- @
+-- allRoutes :: 'MonadRouter' m => 'Handler' m '[] '[] ByteString
+-- allRoutes = ['match'| v1\/users\/userId:Int |]    -- non-TH version: 'path' \@"v1/users" . 'pathVar' \@"userId" \@Int
+--             $ getUser \<|\> putUser \<|\> deleteUser
+--
+-- type IntUserId = 'PathVar' "userId" Int
+--
+-- getUser :: ('MonadRouter' m, 'Has' IntUserId req) => 'Handler' m req '[] ByteString
+-- getUser = 'method' \@GET getUserHandler
+--
+-- putUser :: ('MonadRouter' m, 'Has' IntUserId req) => 'Handler' m req '[] ByteString
+-- putUser = 'method' \@PUT
+--           $ 'requestContentType' \@"application/json"
+--           $ 'jsonRequestBody' \@User
+--           $ putUserHandler
+--
+-- deleteUser :: ('MonadRouter' m, 'Has' IntUserId req) => 'Handler' m req '[] ByteString
+-- deleteUser = 'method' \@DELETE deleteUserHandler
+-- @
+--
+--
+-- $running
+--
+-- Routable handlers can be converted to a regular function using
+-- 'runRoute':
+--
+-- @
+-- runRoute :: Monad m => 'Handler' (RouterT m) '[] res ByteString -> ('Wai.Request' -> m 'Wai.Response')
+-- @
+--
+-- This function converts a WebGear handler to a function from
+-- 'Wai.Request' to 'Wai.Response' in a monadic context @m@. Then it
+-- is trivial to convert that to a WAI 'Wai.Application' and run it as a
+-- warp server:
+--
+-- @
+-- application :: 'Wai.Application'
+-- application req respond = 'runRoute' allRoutes req >>= respond
+--
+-- main :: IO ()
+-- main = Warp.run 3000 application
+-- @
+--
diff --git a/src/WebGear/Middlewares.hs b/src/WebGear/Middlewares.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Middlewares.hs
@@ -0,0 +1,46 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Middlewares provided by WebGear.
+--
+module WebGear.Middlewares
+  ( ok
+  , noContent
+  , badRequest
+  , notFound
+
+  , module WebGear.Middlewares.Method
+  , module WebGear.Middlewares.Path
+  , module WebGear.Middlewares.Header
+  , module WebGear.Middlewares.Body
+  ) where
+
+import Data.String (IsString)
+
+import WebGear.Middlewares.Body
+import WebGear.Middlewares.Header
+import WebGear.Middlewares.Method
+import WebGear.Middlewares.Path
+import WebGear.Trait (Linked, linkzero)
+import WebGear.Types (Response (..))
+
+import qualified Network.HTTP.Types as HTTP
+
+
+-- | Respond with a 200 OK
+ok :: Monad m => a -> m (Linked '[] (Response a))
+ok = pure . linkzero . Response HTTP.ok200 mempty . Just
+
+-- | Respond with a 400 Bad Request
+badRequest :: Monad m => m (Linked '[] (Response a))
+badRequest = pure $ linkzero $ Response HTTP.badRequest400 mempty Nothing
+
+-- | Respond with a 404 NotFound
+notFound :: Monad m => m (Linked '[] (Response a))
+notFound = pure $ linkzero $ Response HTTP.notFound404 mempty Nothing
+
+-- | Respond with a 204 NoContent
+noContent :: (Monad m, IsString s) => m (Linked '[] (Response s))
+noContent = pure $ linkzero $ Response HTTP.noContent204 mempty $ Just ""
diff --git a/src/WebGear/Middlewares/Body.hs b/src/WebGear/Middlewares/Body.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Middlewares/Body.hs
@@ -0,0 +1,64 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Middlewares related to HTTP body.
+module WebGear.Middlewares.Body
+  ( jsonRequestBody
+  , jsonResponseBody
+  ) where
+
+import Control.Arrow (Kleisli (..))
+import Control.Monad ((>=>))
+import Control.Monad.IO.Class (MonadIO)
+import Data.Aeson (FromJSON, ToJSON, encode)
+import Data.ByteString.Lazy (ByteString, fromStrict)
+import Data.HashMap.Strict (fromList, insert)
+import Data.Text (Text)
+import Data.Text.Encoding (encodeUtf8)
+import Network.HTTP.Types (badRequest400, hContentType)
+
+import WebGear.Route (MonadRouter (..))
+import WebGear.Trait (linkplus, linkzero, unlink)
+import WebGear.Trait.Body (JSONRequestBody)
+import WebGear.Types (Middleware, RequestMiddleware, Response (..))
+
+
+-- | A middleware to parse the request body as JSON and convert it to
+-- a value via a 'FromJSON' instance.
+--
+-- Usage for a type @t@ which has a 'FromJSON' instance:
+--
+-- > jsonRequestBody @t handler
+--
+jsonRequestBody :: forall t m req res a. (FromJSON t, MonadRouter m, MonadIO m)
+                => RequestMiddleware m req (JSONRequestBody t:req) res a
+jsonRequestBody handler = Kleisli $
+  linkplus @(JSONRequestBody t) >=> either (failHandler . mkError) (runKleisli handler)
+  where
+    mkError :: Text -> Response ByteString
+    mkError e = Response
+          { respStatus  = badRequest400
+          , respHeaders = fromList []
+          , respBody    = Just $ fromStrict $ encodeUtf8 $ "Error parsing request body: " <> e
+          }
+
+-- | A middleware that converts the response that has a 'ToJSON'
+-- instance to a 'ByteString' response.
+--
+-- This will also set the "Content-Type" header of the response to
+-- "application/json".
+--
+-- Usage for a type @t@ which has a 'ToJSON' instance:
+--
+-- > jsonResponseBody @t handler
+--
+jsonResponseBody :: (ToJSON t, Monad m) => Middleware m req req res '[] t ByteString
+jsonResponseBody handler = Kleisli $ \req -> do
+  x <- unlink <$> runKleisli handler req
+  pure $ linkzero $ Response
+    { respStatus  = respStatus x
+    , respHeaders = insert hContentType "application/json" $ respHeaders x
+    , respBody    = encode <$> respBody x
+    }
diff --git a/src/WebGear/Middlewares/Header.hs b/src/WebGear/Middlewares/Header.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Middlewares/Header.hs
@@ -0,0 +1,47 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Middlewares related to HTTP headers.
+module WebGear.Middlewares.Header
+  ( requestContentType
+  ) where
+
+import Control.Arrow (Kleisli (..))
+import Control.Monad ((>=>))
+import Data.ByteString.Lazy (ByteString)
+import Data.HashMap.Strict (fromList)
+import Data.String (fromString)
+import GHC.TypeLits (KnownSymbol)
+import Network.HTTP.Types (badRequest400)
+import Text.Printf (printf)
+
+import WebGear.Route (MonadRouter (..))
+import WebGear.Trait (linkplus)
+import WebGear.Trait.Header (HeaderMatch, HeaderMismatch (..))
+import WebGear.Types (RequestMiddleware, Response (..))
+
+
+-- | A middleware to check that the Content-Type header in the request
+-- has a specific value. It will fail the handler if the header did
+-- not match.
+--
+-- Typical usage:
+--
+-- > requestContentType @"application/json" handler
+--
+requestContentType :: forall c m req res a. (KnownSymbol c, MonadRouter m)
+                   => RequestMiddleware m req (HeaderMatch "Content-Type" c:req) res a
+requestContentType handler = Kleisli $
+  linkplus @(HeaderMatch "Content-Type" c) >=> either (failHandler . mkError) (runKleisli handler)
+  where
+    mkError :: HeaderMismatch -> Response ByteString
+    mkError err = Response
+                  { respStatus  = badRequest400
+                  , respHeaders = fromList []
+                  , respBody    = Just $ fromString $
+                    case (expectedHeader err, actualHeader err) of
+                      (ex, Nothing) -> printf "Expected Content-Type header %s but not found" (show ex)
+                      (ex, Just h)  -> printf "Expected Content-Type header %s but found %s" (show ex) (show h)
+                  }
diff --git a/src/WebGear/Middlewares/Method.hs b/src/WebGear/Middlewares/Method.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Middlewares/Method.hs
@@ -0,0 +1,32 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Middlewares related to HTTP methods.
+module WebGear.Middlewares.Method
+  ( method
+  ) where
+
+import Control.Arrow (Kleisli (..))
+import Control.Monad ((>=>))
+
+import WebGear.Route (MonadRouter (..))
+import WebGear.Trait (linkplus)
+import WebGear.Trait.Method (IsStdMethod, Method)
+import WebGear.Types (RequestMiddleware)
+
+
+-- | A middleware to check whether the request has a specified HTTP
+-- method.
+--
+-- Typically this would be used with a type application such as:
+--
+-- > method @GET handler
+--
+-- It is also idiomatic to use the template haskell quasiquoter
+-- 'WebGear.Middlewares.Path.match' in cases where both HTTP method
+-- and path needs to be matched.
+method :: forall t m req res a. (IsStdMethod t, MonadRouter m)
+       => RequestMiddleware m req (Method t:req) res a
+method handler = Kleisli $ linkplus @(Method t) >=> either (const rejectRoute) (runKleisli handler)
diff --git a/src/WebGear/Middlewares/Path.hs b/src/WebGear/Middlewares/Path.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Middlewares/Path.hs
@@ -0,0 +1,115 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Middlewares related to route paths.
+module WebGear.Middlewares.Path
+  ( path
+  , pathVar
+  , match
+  ) where
+
+import Control.Arrow (Kleisli (..))
+import Control.Monad ((>=>))
+import Data.Function ((&))
+import Data.List.NonEmpty (NonEmpty (..), toList)
+import GHC.TypeLits (KnownSymbol)
+import Language.Haskell.TH.Quote (QuasiQuoter (..))
+import Language.Haskell.TH.Syntax (Exp (..), Q, TyLit (..), Type (..), mkName)
+import Web.HttpApiData (FromHttpApiData)
+
+import WebGear.Middlewares.Method (method)
+import WebGear.Route (MonadRouter (..))
+import WebGear.Trait (linkplus)
+import WebGear.Trait.Path (Path, PathVar)
+import WebGear.Types (RequestMiddleware)
+import WebGear.Util (splitOn)
+
+import qualified Data.List as List
+
+
+-- | A middleware that literally matches path @s@.
+--
+-- The symbol @s@ could contain one or more parts separated by a
+-- forward slash character. The route will be rejected if there is no
+-- match.
+--
+-- For example, the following code could be used to match the URL path
+-- \"a\/b\/c\" and then invoke @handler@:
+--
+-- > path @"a/b/c" handler
+--
+path :: forall s ts res m a. (KnownSymbol s, MonadRouter m)
+     => RequestMiddleware m ts (Path s:ts) res a
+path handler = Kleisli $
+  linkplus @(Path s) >=> either (const rejectRoute) (runKleisli handler)
+
+-- | A middleware that captures a path variable from a single path
+-- component.
+--
+-- The value captured is converted to a value of type @val@ via
+-- 'FromHttpApiData'. The route will be rejected if the value is not
+-- found or cannot be converted.
+--
+-- For example, the following code could be used to read a path
+-- component as 'Int' tagged with the symbol \"objId\", and then
+-- invoke @handler@:
+--
+-- > pathVar @"objId" @Int handler
+--
+pathVar :: forall tag val ts res m a. (FromHttpApiData val, MonadRouter m)
+        => RequestMiddleware m ts (PathVar tag val:ts) res a
+pathVar handler = Kleisli $
+  linkplus @(PathVar tag val) >=> either (const rejectRoute) (runKleisli handler)
+
+-- | Produces middleware(s) to match an optional HTTP method and path.
+--
+-- This quasiquoter can be used in several ways:
+--
+-- * @[match|a\/b\/c]@ is equivalent to @'path' \@\"a\/b\/c\"@
+-- * @[match|a\/b\/objId:Int\/d]@ is equivalent to
+--   @'path' \@\"a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' @\"d\"@
+-- * @[match|GET a\/b\/c]@ is equivalent to
+--   @'method' \@GET $ 'path' \@\"a\/b\/c\"@
+-- * @[match|GET a\/b\/objId:Int\/d]@ is equivalent to
+--   @'method' \@GET . 'path' \@\"a\/b\" . 'pathVar' \@\"objId\" \@Int . 'path' \@\"d\"@
+--
+match :: QuasiQuoter
+match = QuasiQuoter
+  { quoteExp  = toExp
+  , quotePat  = const $ fail "match cannot be used in a pattern"
+  , quoteType = const $ fail "match cannot be used in a type"
+  , quoteDec  = const $ fail "match cannot be used in a declaration"
+  }
+  where
+    toExp :: String -> Q Exp
+    toExp s = case List.words s of
+      [m, p] -> do
+        let methodExp = AppTypeE (VarE 'method) (ConT $ mkName m)
+        pathExps <- toPathExps p
+        pure $ List.foldr1 compose $ methodExp :| pathExps
+      [p]    -> do
+        pathExps <- toPathExps p
+        pure $ List.foldr1 compose pathExps
+      _      -> fail "match expects an HTTP method and a path or just a path"
+
+    toPathExps :: String -> Q [Exp]
+    toPathExps p = splitOn '/' p
+                   & fmap (splitOn ':')
+                   & List.foldr joinPath []
+                   & fmap toPathExp
+                   & sequence
+
+    joinPath :: NonEmpty String -> [NonEmpty String] -> [NonEmpty String]
+    joinPath s []                    = [s]
+    joinPath (s:|[]) ((s':|[]) : xs) = ((s <> "/" <> s') :| []) : xs
+    joinPath y (x:xs)                = y:x:xs
+
+    toPathExp :: NonEmpty String -> Q Exp
+    toPathExp (p :| [])  = pure $ AppTypeE (VarE 'path) (LitT $ StrTyLit p)
+    toPathExp (v :| [t]) = pure $ AppTypeE (AppTypeE (VarE 'pathVar) (LitT $ StrTyLit v)) (ConT $ mkName t)
+    toPathExp xs         = fail $ "Invalid path component: " <> List.intercalate ":" (toList xs)
+
+    compose :: Exp -> Exp -> Exp
+    compose l = UInfixE l (VarE $ mkName ".")
diff --git a/src/WebGear/Route.hs b/src/WebGear/Route.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Route.hs
@@ -0,0 +1,81 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Types and functions to route HTTP requests.
+module WebGear.Route
+  ( RouterT
+  , MonadRouter (..)
+  , runRoute
+  ) where
+
+import Control.Applicative (Alternative)
+import Control.Arrow (Kleisli (..))
+import Control.Monad (MonadPlus (..))
+import Control.Monad.Except (ExceptT, MonadError (..), runExceptT)
+import Data.ByteString.Lazy (ByteString)
+import Data.HashMap.Strict (fromList)
+import Data.Semigroup (First (..))
+import Data.String (fromString)
+import Data.Version (showVersion)
+import Network.HTTP.Types (Header, hServer, notFound404)
+
+import Paths_webgear_server (version)
+import WebGear.Trait (linkzero, unlink)
+import WebGear.Types (Handler, Response (..), waiResponse)
+
+import qualified Network.Wai as Wai
+
+
+-- | The monad transformer stack for routing.
+--
+-- * The 'ExceptT' provides short-circuiting behaviour for
+--   'rejectRoute' and 'failHandler'.
+--
+-- * In case of 'rejectRoute', a 'Nothing' value is returned and in
+--   case of 'failHandler', a @Response ByteString@ is returned.
+--
+-- * The 'First' wrapper is provided to get instances of 'Alternative'
+--   and 'MonadPlus' for 'RouterT'.
+--
+type RouterT m = ExceptT (Maybe (First (Response ByteString))) m
+
+-- | HTTP request routing with short circuiting behavior.
+class (Alternative m, MonadPlus m) => MonadRouter m where
+  -- | Mark the current route as rejected, alternatives can be tried
+  rejectRoute :: m a
+
+  -- | Short-circuit the current handler and return a response
+  failHandler :: Response ByteString -> m a
+
+instance Monad m => MonadRouter (RouterT m) where
+  rejectRoute :: RouterT m a
+  rejectRoute = mzero
+
+  failHandler :: Response ByteString -> RouterT m a
+  failHandler = throwError . Just . First
+
+-- | Convert a routable handler into a plain function.
+--
+-- This function is typically used to convert WebGear routes to a
+-- 'Wai.Application'.
+runRoute :: Monad m
+         => Handler (RouterT m) '[] res ByteString
+         -> (Wai.Request -> m Wai.Response)
+runRoute route req = waiResponse . addServerHeader . either (maybe notFoundResponse getFirst) id <$> runExceptT f
+  where
+    f = unlink <$> runKleisli route (linkzero req)
+
+    notFoundResponse :: Response ByteString
+    notFoundResponse = Response
+      { respStatus  = notFound404
+      , respHeaders = fromList []
+      , respBody    = Just "Not Found"
+      }
+
+    addServerHeader :: Response ByteString -> Response ByteString
+    addServerHeader r = r { respHeaders = respHeaders r <> fromList [serverHeader] }
+
+    serverHeader :: Header
+    serverHeader = (hServer, fromString $ "WebGear/" ++ showVersion version)
diff --git a/src/WebGear/Trait.hs b/src/WebGear/Trait.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Trait.hs
@@ -0,0 +1,136 @@
+{-# LANGUAGE UndecidableInstances #-}
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Traits are optional attributes that a value might posess. For example,
+-- a list containing totally ordered values might have a @Maximum@ trait
+-- where the associated attribute is the maximum value. The trait exists
+-- only if the list is non-empty.
+--
+-- Traits help to access these attributes in a type-safe manner.
+--
+-- Traits are somewhat similar to [refinement
+-- types](https://hackage.haskell.org/package/refined), but allow
+-- arbitrary attributes to be associated with a value instead of only a
+-- predicate.
+module WebGear.Trait
+  ( -- * Core Types
+    Trait (..)
+  , CheckResult (..)
+  , Linked
+  , Traits
+
+    -- * Linking values with traits
+  , linkzero
+  , linkplus
+  , linkminus
+  , unlink
+
+    -- * Retrive trait attributes from linked values
+  , Has (..)
+  , Have
+  ) where
+
+import Data.Kind (Constraint, Type)
+import Data.Tagged (Tagged (..))
+
+
+-- | A 'Trait' is an optional attribute @t@ associated with a value @a@.
+--
+-- The 'check' function validates the presence of the trait for a
+-- given value. Checking the presence of the trait can optionally
+-- modify the value as well.
+class Monad m => Trait t a m where
+  -- | Type of the associated attribute
+  type Val t a
+
+  -- | Type of check failures
+  type Fail t a
+
+  -- | Checks the presence of the associated attribute.
+  check :: a -> m (CheckResult t a)
+
+-- | Result of a 'check' operation
+data CheckResult t a = CheckSuccess a (Val t a)
+                     | CheckFail (Fail t a)
+
+deriving instance (Eq a, Eq (Val t a), Eq (Fail t a)) => Eq (CheckResult t a)
+deriving instance (Show a, Show (Val t a), Show (Fail t a)) => Show (CheckResult t a)
+deriving instance (Read a, Read (Val t a), Read (Fail t a)) => Read (CheckResult t a)
+
+
+-- | A trivial trait that is always present and whose attribute does
+-- not carry any meaningful information.
+instance Monad m => Trait '[] a m where
+  type Val '[] a = ()
+  type Fail '[] a = ()
+
+  check :: a -> m (CheckResult '[] a)
+  check a = pure $ CheckSuccess a ()
+
+-- | Combination of many traits all of which are present for a value.
+instance (Trait t a m, Trait ts a m) => Trait (t:ts) a m where
+  type Val (t:ts) a = (Val t a, Val ts a)
+  type Fail (t:ts) a = Either (CheckResult t a) (CheckResult ts a)
+
+  check :: a -> m (CheckResult (t:ts) a)
+  check a = check @t a >>= \case
+    e@(CheckFail _)   -> pure $ CheckFail $ Left e
+    CheckSuccess a' l -> check @ts a' >>= \case
+      e@(CheckFail _)    -> pure $ CheckFail $ Right e
+      CheckSuccess a'' r -> pure $ CheckSuccess a'' (l, r)
+
+-- | Constraint for functions that use multiple traits
+type family Traits ts a m :: Constraint where
+  Traits '[]    a m = ()
+  Traits (t:ts) a m = (Trait t a m, Traits ts a m)
+
+-- | A value linked with a type-level list of traits.
+data Linked (ts :: [Type]) a = Linked
+    { linkVal :: !(Val ts a)
+    , unlink  :: !a           -- ^ Retrive the value from a linked value
+    }
+
+-- | Link a value with the trivial trait
+linkzero :: a -> Linked '[] a
+linkzero = Linked ()
+
+-- | Attempt to link an additional trait with an already linked value
+linkplus :: Trait t a m => Linked ts a -> m (Either (Fail t a) (Linked (t:ts) a))
+linkplus l = do
+  v <- check (unlink l)
+  pure $ mkLinked v l
+  where
+    mkLinked :: CheckResult t a -> Linked ts a -> Either (Fail t a) (Linked (t:ts) a)
+    mkLinked (CheckSuccess a left) lv = Right $ Linked (left, linkVal lv) a
+    mkLinked (CheckFail e) _          = Left e
+
+-- | Remove the leading trait from the linked value
+linkminus :: Linked (t:ts) a -> Linked ts a
+linkminus l = Linked (snd $ linkVal l) (unlink l)
+
+
+-- | Constraint that proves that the trait @t@ is present somewhere in
+-- the list of traits @ts@.
+class Has t ts where
+  traitValue :: Linked ts a -> Tagged t (Val t a)
+
+instance Has t (t:ts) where
+  traitValue :: Linked (t : ts) a -> Tagged t (Val t a)
+  traitValue (Linked (lv, _) _) = Tagged lv
+
+instance {-# OVERLAPPABLE #-} Has t ts => Has t (t':ts) where
+  traitValue :: Linked (t':ts) a -> Tagged t (Val t a)
+  traitValue l = traitValue (rightLinked l)
+    where
+      rightLinked :: Linked (q:qs) b -> Linked qs b
+      rightLinked (Linked (_, rv) a) = Linked rv a
+
+
+-- | Constraint that proves that all the traits in the list @ts@ are
+-- present in the list @qs@.
+type family Have ts qs :: Constraint where
+  Have '[]    qs = ()
+  Have (t:ts) qs = (Has t qs, Have ts qs)
diff --git a/src/WebGear/Trait/Body.hs b/src/WebGear/Trait/Body.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Trait/Body.hs
@@ -0,0 +1,34 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Traits related to HTTP body.
+module WebGear.Trait.Body
+  ( JSONRequestBody
+  ) where
+
+import Control.Monad.IO.Class (MonadIO (..))
+import Data.Aeson (FromJSON, eitherDecode')
+import Data.ByteString.Lazy (fromChunks)
+import Data.Kind (Type)
+import Data.Text (Text, pack)
+
+import WebGear.Trait (CheckResult (..), Trait (..))
+import WebGear.Types (Request, getRequestBodyChunk)
+import WebGear.Util (takeWhileM)
+
+
+-- | A 'Trait' for converting a JSON request body into a value.
+data JSONRequestBody (t :: Type)
+
+instance (FromJSON t, MonadIO m) => Trait (JSONRequestBody t) Request m where
+  type Val (JSONRequestBody t) Request = t
+  type Fail (JSONRequestBody t) Request = Text
+
+  check :: Request -> m (CheckResult (JSONRequestBody t) Request)
+  check r = do
+    chunks <- takeWhileM (/= mempty) $ repeat $ liftIO $ getRequestBodyChunk r
+    pure $ case eitherDecode' (fromChunks chunks) of
+             Left e  -> CheckFail (pack e)
+             Right t -> CheckSuccess r t
diff --git a/src/WebGear/Trait/Header.hs b/src/WebGear/Trait/Header.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Trait/Header.hs
@@ -0,0 +1,71 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Traits related to HTTP headers.
+module WebGear.Trait.Header
+  ( Header
+  , HeaderFail (..)
+  , HeaderMatch
+  , HeaderMismatch (..)
+  ) where
+
+import Data.ByteString (ByteString)
+import Data.Kind (Type)
+import Data.Proxy (Proxy (..))
+import Data.String (fromString)
+import Data.Text (Text)
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+import Web.HttpApiData (FromHttpApiData (..))
+
+import WebGear.Trait (CheckResult (..), Trait (..))
+import WebGear.Types (Request, requestHeader)
+
+
+-- | A 'Trait' for capturing a header with name @s@ in a request or
+-- response and convert it to some type @t@ via 'FromHttpApiData'.
+data Header (s :: Symbol) (t :: Type)
+
+-- | Failure in extracting a header value
+data HeaderFail = HeaderNotFound | HeaderParseError Text
+  deriving stock (Read, Show, Eq)
+
+instance (KnownSymbol s, FromHttpApiData t, Monad m) => Trait (Header s t) Request m where
+  type Val (Header s t) Request = t
+  type Fail (Header s t) Request = HeaderFail
+
+  check :: Request -> m (CheckResult (Header s t) Request)
+  check r = pure $
+    let s = fromString $ symbolVal (Proxy @s)
+    in case parseHeader <$> requestHeader s r of
+         Nothing        -> CheckFail HeaderNotFound
+         Just (Left e)  -> CheckFail $ HeaderParseError e
+         Just (Right x) -> CheckSuccess r x
+
+-- | A 'Trait' for ensuring that a header named @s@ has value @t@.
+data HeaderMatch (s :: Symbol) (t :: Symbol)
+
+-- | Failure in extracting a header value
+data HeaderMismatch = HeaderMismatch
+  { expectedHeader :: ByteString
+  , actualHeader   :: Maybe ByteString
+  }
+  deriving stock (Eq, Read, Show)
+
+instance (KnownSymbol s, KnownSymbol t, Monad m) => Trait (HeaderMatch s t) Request m where
+  type Val (HeaderMatch s t) Request = ByteString
+  type Fail (HeaderMatch s t) Request = HeaderMismatch
+
+  check :: Request -> m (CheckResult (HeaderMatch s t) Request)
+  check r = pure $
+    let
+      name = fromString $ symbolVal (Proxy @s)
+      expected = fromString $ symbolVal (Proxy @t)
+    in
+      case requestHeader name r of
+        Nothing                  -> CheckFail HeaderMismatch
+                                      {expectedHeader = expected, actualHeader = Nothing}
+        Just hv | hv == expected -> CheckSuccess r hv
+                | otherwise      -> CheckFail HeaderMismatch
+                                      {expectedHeader = expected, actualHeader = Just hv}
diff --git a/src/WebGear/Trait/Method.hs b/src/WebGear/Trait/Method.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Trait/Method.hs
@@ -0,0 +1,68 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Trait capturing the HTTP method in a request.
+module WebGear.Trait.Method
+  ( Method
+  , IsStdMethod (..)
+  , MethodMismatch (..)
+  ) where
+
+import Data.Proxy (Proxy (..))
+
+import WebGear.Trait (CheckResult (..), Trait (..))
+import WebGear.Types (Request, requestMethod)
+
+import qualified Network.HTTP.Types as HTTP
+
+
+-- | A 'Trait' for capturing the HTTP method of a request
+data Method (t :: HTTP.StdMethod)
+
+-- | Failure to match method against an expected value
+data MethodMismatch = MethodMismatch
+  { expectedMethod :: HTTP.Method
+  , actualMethod   :: HTTP.Method
+  }
+
+instance (Monad m, IsStdMethod t) => Trait (Method t) Request m where
+  type Val (Method t) Request = HTTP.Method
+  type Fail (Method t) Request = MethodMismatch
+
+  check :: Request -> m (CheckResult (Method t) Request)
+  check r =
+    let
+      expected = HTTP.renderStdMethod $ toStdMethod $ Proxy @t
+      actual = requestMethod r
+    in
+      pure $ if expected == actual
+             then CheckSuccess r actual
+             else CheckFail $ MethodMismatch expected actual
+
+
+-- | A typeclass implemented by all 'HTTP.StdMethod's to convert them
+-- from type level to term level.
+class IsStdMethod t where
+  -- | Convert @t@ to term level.
+  toStdMethod :: Proxy t -> HTTP.StdMethod
+
+instance IsStdMethod HTTP.GET where
+  toStdMethod = const HTTP.GET
+instance IsStdMethod HTTP.POST where
+  toStdMethod = const HTTP.POST
+instance IsStdMethod HTTP.HEAD where
+  toStdMethod = const HTTP.HEAD
+instance IsStdMethod HTTP.PUT where
+  toStdMethod = const HTTP.PUT
+instance IsStdMethod HTTP.DELETE where
+  toStdMethod = const HTTP.DELETE
+instance IsStdMethod HTTP.TRACE where
+  toStdMethod = const HTTP.TRACE
+instance IsStdMethod HTTP.CONNECT where
+  toStdMethod = const HTTP.CONNECT
+instance IsStdMethod HTTP.OPTIONS where
+  toStdMethod = const HTTP.OPTIONS
+instance IsStdMethod HTTP.PATCH where
+  toStdMethod = const HTTP.PATCH
diff --git a/src/WebGear/Trait/Path.hs b/src/WebGear/Trait/Path.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Trait/Path.hs
@@ -0,0 +1,66 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Traits related to the route path of a request.
+module WebGear.Trait.Path
+  ( Path
+  , PathVar
+  , PathVarFail (..)
+  ) where
+
+import Data.Kind (Type)
+import Data.List (stripPrefix)
+import Data.List.NonEmpty (toList)
+import Data.Proxy (Proxy (..))
+import Data.Text (Text, pack)
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+import Web.HttpApiData (FromHttpApiData (..))
+
+import WebGear.Trait (CheckResult (..), Trait (..))
+import WebGear.Types (Request, pathInfo, setPathInfo)
+import WebGear.Util (splitOn)
+
+
+-- | A path component which is literally matched against the request
+-- but discarded after that.
+data Path (s :: Symbol)
+
+instance (KnownSymbol s, Monad m) => Trait (Path s) Request m where
+  type Val (Path s) Request = ()
+
+  -- | The path that could not be matched
+  type Fail (Path s) Request = ()
+
+  check :: Request -> m (CheckResult (Path s) Request)
+  check r = pure $
+    let expected = map pack $ toList $ splitOn '/' $ symbolVal $ Proxy @s
+        actual = pathInfo r
+    in
+      case stripPrefix expected actual of
+        Nothing   -> CheckFail ()
+        Just rest -> CheckSuccess (setPathInfo rest r) ()
+
+
+-- | A path variable that is extracted and converted to a value of
+-- type @val@. The @tag@ is usually a type-level symbol (string) to
+-- uniquely identify this variable.
+data PathVar tag (val :: Type)
+
+-- | Failure to extract a 'PathVar'
+data PathVarFail = PathVarNotFound | PathVarParseError Text
+  deriving (Eq, Show, Read)
+
+instance (FromHttpApiData val, Monad m) => Trait (PathVar tag val) Request m where
+  type Val (PathVar tag val) Request = val
+  type Fail (PathVar tag val) Request = PathVarFail
+
+  check :: Request -> m (CheckResult (PathVar tag val) Request)
+  check r = pure $
+    case pathInfo r of
+      []     -> CheckFail PathVarNotFound
+      (x:xs) ->
+        case parseUrlPiece @val x of
+          Left e  -> CheckFail $ PathVarParseError e
+          Right h -> CheckSuccess (setPathInfo xs r) h
diff --git a/src/WebGear/Types.hs b/src/WebGear/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Types.hs
@@ -0,0 +1,107 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Common types and functions used throughout WebGear.
+module WebGear.Types
+  ( -- * WebGear Request
+    -- | WebGear requests are WAI requests. This module reexports a number
+    -- of useful functions that operate on requests from "Network.Wai"
+    -- module.
+    Request
+  , remoteHost
+  , httpVersion
+  , isSecure
+  , requestMethod
+  , pathInfo
+  , setPathInfo
+  , queryString
+  , requestHeaders
+  , requestHeader
+  , requestBodyLength
+  , getRequestBodyChunk
+
+    -- * WebGear Response
+  , Response (..)
+  , waiResponse
+  , addResponseHeader
+
+  , Handler
+  , Middleware
+  , RequestMiddleware
+  , ResponseMiddleware
+  ) where
+
+import Control.Arrow (Kleisli)
+import Data.ByteString (ByteString)
+import Data.List (find)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Network.HTTP.Types (Header, HeaderName, Status)
+import Network.Wai (Request, getRequestBodyChunk, httpVersion, isSecure, pathInfo, queryString,
+                    remoteHost, requestBodyLength, requestHeaders, requestMethod)
+
+import WebGear.Trait (Linked)
+
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.HashMap.Strict as HM
+import qualified Network.Wai as Wai
+
+
+-- | Get the value of a request header
+requestHeader :: HeaderName -> Request -> Maybe ByteString
+requestHeader h r = snd <$> find ((== h) . fst) (requestHeaders r)
+
+-- | Get request with an updated URL path info.
+setPathInfo :: [Text] -> Request -> Request
+setPathInfo p r = r { pathInfo = p }
+
+-- | A response sent from the server to the client.
+--
+-- The response contains a status, optional headers and an optional
+-- body of type @a@.
+data Response a = Response
+    { respStatus  :: Status                            -- ^ Response status code
+    , respHeaders :: HM.HashMap HeaderName ByteString  -- ^ Response headers
+    , respBody    :: Maybe a                           -- ^ Optional response body
+    }
+
+-- | Convert a WebGear response to a WAI Response.
+waiResponse :: Response LBS.ByteString -> Wai.Response
+waiResponse Response{..} = Wai.responseLBS respStatus (HM.toList respHeaders) (fromMaybe "" respBody)
+
+-- | Create or update a response header.
+addResponseHeader :: Header -> Response a -> Response a
+addResponseHeader (name, val) resp = resp { respHeaders = HM.insertWith f name val (respHeaders resp) }
+  where
+    f = flip const
+
+-- | A handler is a function from a request to response in a monadic
+-- context. Both the request and the response can have linked traits.
+--
+-- The type level list @req@ contains all the traits expected to be
+-- present in the request. The handler will produce a response that
+-- satisfies all the traits in the type level list @res@.
+type Handler m req res a = Kleisli m (Linked req Request) (Linked res (Response a))
+
+-- | A middleware takes a handler as input and produces another
+-- handler that usually adds some functionality.
+--
+-- A middleware can do a number of things with the request
+-- handling such as:
+--
+--   * Change the request traits before invoking the handler.
+--   * Change the response traits before passing it back to its caller.
+--   * Use the linked value of any of the request or response traits.
+--   * Change the response body.
+--
+type Middleware m req req' res' res a' a = Handler m req' res' a' -> Handler m req res a
+
+-- | A middleware that manipulates only the request traits and leaves
+-- the response unchanged.
+type RequestMiddleware m req req' res a = Middleware m req req' res res a a
+
+-- | A middleware that manipulates only the response traits and leaves
+-- the request unchanged.
+type ResponseMiddleware m req res' res a = Middleware m req req res' res a a
diff --git a/src/WebGear/Util.hs b/src/WebGear/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/WebGear/Util.hs
@@ -0,0 +1,27 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+-- Common utility functions.
+module WebGear.Util
+  ( takeWhileM
+  , splitOn
+  ) where
+
+import Data.List.NonEmpty (NonEmpty (..), toList)
+
+
+takeWhileM :: Monad m => (a -> Bool) -> [m a] -> m [a]
+takeWhileM _    []     = pure []
+takeWhileM p (mx:mxs) = do
+  x <- mx
+  if p x
+    then (x :) <$> takeWhileM p mxs
+    else pure []
+
+splitOn :: Eq a => a -> [a] -> NonEmpty [a]
+splitOn sep = foldr f ([] :| [])
+  where
+    f x acc       | x == sep = [] :| toList acc
+    f x (y :| ys) = (x:y) :| ys
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,21 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+module Main where
+
+import Test.Tasty (TestTree, defaultMain, testGroup)
+
+import Properties (propertyTests)
+import Unit (unitTests)
+
+
+main :: IO ()
+main = defaultMain allTests
+
+allTests :: TestTree
+allTests = testGroup "Tests" [unitTests, propertyTests, systemTests]
+
+systemTests :: TestTree
+systemTests = testGroup "System Tests" []
diff --git a/test/Properties.hs b/test/Properties.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties.hs
@@ -0,0 +1,23 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+module Properties
+  ( propertyTests
+  ) where
+
+import Test.Tasty (TestTree, testGroup)
+
+import qualified Properties.Trait.Body as Body
+import qualified Properties.Trait.Header as Header
+import qualified Properties.Trait.Method as Method
+import qualified Properties.Trait.Path as Path
+
+
+propertyTests :: TestTree
+propertyTests = testGroup "Property Tests" [ Body.tests
+                                           , Header.tests
+                                           , Method.tests
+                                           , Path.tests
+                                           ]
diff --git a/test/Properties/Trait/Body.hs b/test/Properties/Trait/Body.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties/Trait/Body.hs
@@ -0,0 +1,53 @@
+{-# OPTIONS_GHC -Wno-deprecations #-}
+
+module Properties.Trait.Body
+  ( tests
+  ) where
+
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Data.IORef (newIORef, readIORef, writeIORef)
+import Data.String (fromString)
+import Network.Wai (Request, defaultRequest, requestBody)
+import Test.QuickCheck (Property, allProperties, counterexample, property)
+import Test.QuickCheck.Instances ()
+import Test.QuickCheck.Monadic (assert, monadicIO, monitor)
+import Test.Tasty (TestTree)
+import Test.Tasty.QuickCheck (testProperties)
+
+import WebGear.Trait
+import WebGear.Trait.Body
+
+
+bodyToRequest :: (MonadIO m, Show a) => a -> m Request
+bodyToRequest x = do
+  body <- liftIO $ newIORef $ Just $ fromString $ show x
+  let f = readIORef body >>= maybe (pure "") (\s -> writeIORef body Nothing >> pure s)
+  return defaultRequest { requestBody = f }
+
+prop_emptyRequestBodyFails :: Property
+prop_emptyRequestBodyFails = monadicIO $ do
+  req <- bodyToRequest ("" :: String)
+  check @(JSONRequestBody Int) req >>= \case
+    CheckSuccess _ _ -> monitor (counterexample "Unexpected success") >> assert False
+    CheckFail _      -> assert True
+
+prop_validBodyParses :: Property
+prop_validBodyParses = property $ \n -> monadicIO $ do
+  req <- bodyToRequest (n :: Integer)
+  check @(JSONRequestBody Integer) req >>= \case
+    CheckSuccess _ n' -> assert (n == n')
+    CheckFail _       -> assert False
+
+prop_invalidBodyFails :: Property
+prop_invalidBodyFails = property $ \n -> monadicIO $ do
+  req <- bodyToRequest (n :: Integer)
+  check @(JSONRequestBody String) req >>= \case
+    CheckSuccess _ _ -> assert False
+    CheckFail _      -> assert True
+
+
+-- Hack for TH splicing
+return []
+
+tests :: TestTree
+tests = testProperties "Trait.Body" $allProperties
diff --git a/test/Properties/Trait/Header.hs b/test/Properties/Trait/Header.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties/Trait/Header.hs
@@ -0,0 +1,55 @@
+module Properties.Trait.Header
+  ( tests
+  ) where
+
+import Data.Functor.Identity (runIdentity)
+import Data.String (fromString)
+import Data.Text.Encoding (encodeUtf8)
+import Network.Wai (defaultRequest, requestHeaders)
+import Test.QuickCheck (Property, allProperties, counterexample, property, (.&&.), (=/=), (===))
+import Test.QuickCheck.Instances ()
+import Test.Tasty (TestTree)
+import Test.Tasty.QuickCheck (testProperties)
+
+import WebGear.Trait
+import WebGear.Trait.Header
+
+
+prop_headerParseError :: Property
+prop_headerParseError = property $ \hval ->
+  let
+    hval' = "test-" <> hval
+    req = defaultRequest { requestHeaders = [("foo", encodeUtf8 hval')] }
+  in
+    case runIdentity (check @(Header "foo" Int) req) of
+      CheckFail e      ->
+        e === HeaderParseError ("could not parse: `" <> hval' <> "' (input does not start with a digit)")
+      CheckSuccess _ v ->
+        counterexample ("Unexpected result: " <> show v) (property False)
+
+prop_headerParseSuccess :: Property
+prop_headerParseSuccess = property $ \(n :: Int) ->
+  let
+    req = defaultRequest { requestHeaders = [("foo", fromString $ show n)] }
+  in
+    case runIdentity (check @(Header "foo" Int) req) of
+      CheckFail e       ->
+        counterexample ("Unexpected result: " <> show e) (property False)
+      CheckSuccess _ n' -> n === n'
+
+prop_headerMatch :: Property
+prop_headerMatch = property $ \v ->
+  let
+    req = defaultRequest { requestHeaders = [("foo", v)] }
+  in
+    case runIdentity (check @(HeaderMatch "foo" "bar") req) of
+      CheckFail e       ->
+        expectedHeader e === "bar" .&&. actualHeader e =/= Nothing .&&. actualHeader e =/= Just "bar"
+      CheckSuccess _ v' -> v === "bar" .&&. v === v'
+
+
+-- Hack for TH splicing
+return []
+
+tests :: TestTree
+tests = testProperties "Trait.Header" $allProperties
diff --git a/test/Properties/Trait/Method.hs b/test/Properties/Trait/Method.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties/Trait/Method.hs
@@ -0,0 +1,38 @@
+module Properties.Trait.Method
+  ( tests
+  ) where
+
+import Data.Functor.Identity (runIdentity)
+import Network.HTTP.Types (StdMethod (..), methodGet, renderStdMethod)
+import Network.Wai (defaultRequest, requestMethod)
+import Test.QuickCheck (Arbitrary (arbitrary), Property, allProperties, elements, property, (.&&.),
+                        (=/=), (===))
+import Test.Tasty (TestTree)
+import Test.Tasty.QuickCheck (testProperties)
+
+import WebGear.Trait
+import WebGear.Trait.Method
+
+
+newtype MethodWrapper = MethodWrapper StdMethod
+  deriving stock (Show)
+
+instance Arbitrary MethodWrapper where
+  arbitrary = elements $ MethodWrapper <$> [minBound..maxBound]
+
+prop_methodMatch :: Property
+prop_methodMatch = property $ \(MethodWrapper v) ->
+  let
+    req = defaultRequest { requestMethod = renderStdMethod v }
+  in
+    case runIdentity (check @(Method GET) req) of
+      CheckSuccess _ v' -> v === GET .&&. v' === methodGet
+      CheckFail e       ->
+        expectedMethod e === methodGet .&&. actualMethod e =/= methodGet
+
+
+-- Hack for TH splicing
+return []
+
+tests :: TestTree
+tests = testProperties "Trait.Method" $allProperties
diff --git a/test/Properties/Trait/Path.hs b/test/Properties/Trait/Path.hs
new file mode 100644
--- /dev/null
+++ b/test/Properties/Trait/Path.hs
@@ -0,0 +1,52 @@
+module Properties.Trait.Path
+  ( tests
+  ) where
+
+import Data.Functor.Identity (runIdentity)
+import Data.String (fromString)
+import Network.Wai (defaultRequest, pathInfo)
+import Test.QuickCheck (Property, allProperties, property, (.&&.), (=/=), (===))
+import Test.QuickCheck.Instances ()
+import Test.Tasty (TestTree)
+import Test.Tasty.QuickCheck (testProperties)
+
+import WebGear.Trait
+import WebGear.Trait.Path
+
+
+prop_pathMatch :: Property
+prop_pathMatch = property $ \h ->
+  let
+    rest = ["foo", "bar"]
+    req = defaultRequest { pathInfo = h:rest }
+  in
+    case runIdentity (check @(Path "a") req) of
+      CheckSuccess req' _ -> h === "a" .&&. pathInfo req' === rest
+      CheckFail _         -> h =/= "a"
+
+prop_pathVarMatch :: Property
+prop_pathVarMatch = property $ \(n :: Int) ->
+  let
+    rest = ["foo", "bar"]
+    req = defaultRequest { pathInfo = fromString (show n):rest }
+  in
+    case runIdentity (check @(PathVar "tag" Int) req) of
+      CheckSuccess req' n' -> n' === n .&&. pathInfo req' === rest
+      CheckFail _          -> property False
+
+prop_pathVarParseError :: Property
+prop_pathVarParseError = property $ \(p, ps) ->
+  let
+    p' = "test-" <> p
+    req = defaultRequest { pathInfo = p':ps }
+  in
+    case runIdentity (check @(PathVar "tag" Int) req) of
+      CheckSuccess _ _ -> property False
+      CheckFail e      -> e === PathVarParseError ("could not parse: `" <> p' <> "' (input does not start with a digit)")
+
+
+-- Hack for TH splicing
+return []
+
+tests :: TestTree
+tests = testProperties "Trait.Path" $allProperties
diff --git a/test/Unit.hs b/test/Unit.hs
new file mode 100644
--- /dev/null
+++ b/test/Unit.hs
@@ -0,0 +1,19 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+module Unit
+  ( unitTests
+  ) where
+
+import Test.Tasty (TestTree, testGroup)
+
+import qualified Unit.Trait.Header as Header
+import qualified Unit.Trait.Path as Path
+
+
+unitTests :: TestTree
+unitTests = testGroup "Unit Tests" [ Header.tests
+                                   , Path.tests
+                                   ]
diff --git a/test/Unit/Trait/Header.hs b/test/Unit/Trait/Header.hs
new file mode 100644
--- /dev/null
+++ b/test/Unit/Trait/Header.hs
@@ -0,0 +1,45 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+module Unit.Trait.Header
+  ( tests
+  ) where
+
+import Network.Wai (defaultRequest, requestHeaders)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
+
+import WebGear.Trait
+import WebGear.Trait.Header
+
+
+testMissingHeaderFails :: TestTree
+testMissingHeaderFails = testCase "Missing header fails Header trait" $ do
+  let req = defaultRequest { requestHeaders = [] }
+  check @(Header "foo" Int) req >>= \case
+    CheckSuccess _ _ -> assertFailure "unexpected success"
+    CheckFail e      -> e @?= HeaderNotFound
+
+testHeaderMatchPositive :: TestTree
+testHeaderMatchPositive = testCase "Header match: positive" $ do
+  let req = defaultRequest { requestHeaders = [("foo", "bar")] }
+  check @(HeaderMatch "foo" "bar") req >>= \case
+    CheckSuccess _ v -> v @?= "bar"
+    CheckFail e      -> assertFailure $ "Unexpected result: " <> show e
+
+testHeaderMatchMissingHeader :: TestTree
+testHeaderMatchMissingHeader = testCase "Header match: missing header" $ do
+  let req = defaultRequest { requestHeaders = [] }
+  check @(HeaderMatch "foo" "bar") req >>= \case
+    CheckSuccess _ _ -> assertFailure "unexpected success"
+    CheckFail e      -> e @?= HeaderMismatch { expectedHeader = "bar"
+                                             , actualHeader = Nothing
+                                             }
+
+tests :: TestTree
+tests = testGroup "Trait.Header" [ testMissingHeaderFails
+                                 , testHeaderMatchPositive
+                                 , testHeaderMatchMissingHeader
+                                 ]
diff --git a/test/Unit/Trait/Path.hs b/test/Unit/Trait/Path.hs
new file mode 100644
--- /dev/null
+++ b/test/Unit/Trait/Path.hs
@@ -0,0 +1,26 @@
+-- |
+-- Copyright        : (c) Raghu Kaippully, 2020
+-- License          : MPL-2.0
+-- Maintainer       : rkaippully@gmail.com
+--
+module Unit.Trait.Path
+  ( tests
+  ) where
+
+import Network.Wai (defaultRequest, pathInfo)
+import Test.Tasty (TestTree, testGroup)
+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
+
+import WebGear.Trait
+import WebGear.Trait.Path
+
+
+testMissingPathVar :: TestTree
+testMissingPathVar = testCase "PathVar match: missing variable" $ do
+  let req = defaultRequest { pathInfo = [] }
+  check @(PathVar "tag" Int) req >>= \case
+    CheckSuccess _ _ -> assertFailure "unexpected success"
+    CheckFail e      -> e @?= PathVarNotFound
+
+tests :: TestTree
+tests = testGroup "Trait.Path" [ testMissingPathVar ]
diff --git a/webgear-server.cabal b/webgear-server.cabal
new file mode 100644
--- /dev/null
+++ b/webgear-server.cabal
@@ -0,0 +1,130 @@
+cabal-version:       2.4
+name:                webgear-server
+version:             0.1.0
+synopsis:            Composable, type-safe library to build HTTP API servers
+description:
+        WebGear is a library to for building composable, type-safe HTTP API servers.
+
+        WebGear focuses on good documentation and usability.
+
+        See the documentation of WebGear module to get started.
+homepage:            https://github.com/rkaippully/webgear#readme
+bug-reports:         https://github.com/rkaippully/webgear/issues
+author:              Raghu Kaippully
+maintainer:          rkaippully@gmail.com
+copyright:           2020 Raghu Kaippully
+license:             MPL-2.0
+license-file:        LICENSE
+category:            Web
+build-type:          Simple
+extra-source-files:  README.md
+                     ChangeLog.md
+
+
+source-repository head
+  type:      git
+  location:  https://github.com/rkaippully/webgear
+
+
+common webgear-common
+  default-language:   Haskell2010
+  default-extensions: ApplicativeDo
+                      BangPatterns
+                      ConstraintKinds
+                      DataKinds
+                      DefaultSignatures
+                      DeriveAnyClass
+                      DeriveFoldable
+                      DeriveFunctor
+                      DeriveGeneric
+                      DeriveLift
+                      DeriveTraversable
+                      DerivingStrategies
+                      DerivingVia
+                      EmptyCase
+                      ExistentialQuantification
+                      FlexibleContexts
+                      FlexibleInstances
+                      FunctionalDependencies
+                      GADTs
+                      GeneralizedNewtypeDeriving
+                      InstanceSigs
+                      KindSignatures
+                      LambdaCase
+                      MultiParamTypeClasses
+                      MultiWayIf
+                      NamedFieldPuns
+                      OverloadedStrings
+                      PatternSynonyms
+                      PolyKinds
+                      RankNTypes
+                      RecordWildCards
+                      ScopedTypeVariables
+                      StandaloneDeriving
+                      TemplateHaskell
+                      TupleSections
+                      TypeApplications
+                      TypeFamilies
+                      TypeFamilyDependencies
+                      TypeOperators
+  build-depends:      base                  >=4.13.0.0 && <5
+                    , template-haskell      >=2.15.0.0 && <3
+                    , mtl                   ==2.2.*
+                    , unordered-containers  ==0.2.*
+                    , wai                   ==3.2.*
+                    , bytestring            ==0.10.*
+                    , text                  ==1.2.*
+                    , http-types            ==0.12.*
+                    , http-api-data         ==0.4.*
+                    , aeson                 ==1.4.*
+                    , tagged                ==0.8.*
+  ghc-options:        -fwrite-ide-info
+                      -hiedir=.hie
+                      -Wall
+                      -Wno-unticked-promoted-constructors
+                      -Wincomplete-record-updates
+                      -Wincomplete-uni-patterns
+                      -Wredundant-constraints
+
+library
+  import:             webgear-common
+  exposed-modules:    WebGear
+                    , WebGear.Trait
+                    , WebGear.Trait.Method
+                    , WebGear.Trait.Header
+                    , WebGear.Trait.Path
+                    , WebGear.Trait.Body
+                    , WebGear.Types
+                    , WebGear.Route
+                    , WebGear.Middlewares
+                    , WebGear.Middlewares.Body
+                    , WebGear.Middlewares.Header
+                    , WebGear.Middlewares.Method
+                    , WebGear.Middlewares.Path
+  other-modules:      Paths_webgear_server
+                    , WebGear.Util
+  autogen-modules:    Paths_webgear_server
+  hs-source-dirs:     src
+
+test-suite webgear-server-test
+  import:             webgear-common
+  type:               exitcode-stdio-1.0
+  main-is:            Main.hs
+  other-modules:      Unit
+                    , Unit.Trait.Header
+                    , Unit.Trait.Path
+                    , Properties
+                    , Properties.Trait.Body
+                    , Properties.Trait.Header
+                    , Properties.Trait.Method
+                    , Properties.Trait.Path
+  hs-source-dirs:     test
+  ghc-options:        -threaded
+                      -rtsopts
+                      -with-rtsopts=-N
+  build-depends:      tasty                 ==1.2.*
+                    , tasty-quickcheck      ==0.10.*
+                    , tasty-hunit           ==0.10.*
+                    , QuickCheck            ==2.13.*
+                    , quickcheck-instances  ==0.3.*
+                    , webgear-server
