diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for trasa-form
+
+## 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) 2019, goolord
+
+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 goolord 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/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,211 @@
+{-# language DataKinds #-}
+{-# language GADTs #-}
+{-# language KindSignatures #-}
+{-# language OverloadedStrings #-}
+{-# language ScopedTypeVariables #-}
+{-# language TypeApplications #-}
+{-# language TypeOperators #-}
+{-# language TypeFamilies #-}
+{-# language PolyKinds #-}
+
+module Main where
+
+import Data.ByteString.Lazy (ByteString)
+import Data.Functor.Identity
+import Data.Kind (Type)
+import Data.Text (Text)
+import Ditto (Result(..))
+import Ditto.Core 
+import Ditto.Lucid
+import Ditto.Lucid.Named
+import Lucid
+import Network.Wai (Application)
+import Network.Wai.Handler.Warp (run)
+import Network.Wai.Middleware.RequestLogger (logStdoutDev)
+import Trasa.Core
+import Trasa.Extra
+import Trasa.Form
+import Trasa.Form.Lucid
+import Trasa.Server
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as TE
+import qualified Data.Text.Read as TR
+import qualified Text.Read
+import qualified Trasa.Method as Method
+
+tshow :: Show a => a -> Text
+tshow = T.pack . show
+
+readInt :: Text -> Either Text Int
+readInt input = case (TR.signed TR.decimal) input of
+  Left err -> Left $ T.pack err
+  Right (i, _) -> Right i
+
+data Foo = Foo Int Bool Int
+  deriving Show
+
+-- Our route data type. We define this ourselves.
+data Route :: [Type] -> [Param] -> Bodiedness -> Type -> Type where
+  HelloWorld :: Route 
+    '[ByteString] -- ^ now the path captures the first piece as a ByeString
+    '[('Optional ByteString)] -- ^ there is an optional query parameter, decoded as a ByteString
+    'Bodyless -- ^ the route does not have a request body
+    ByteString -- ^ the response body will be `ByteString`
+  FormTest :: Route
+    '[]
+    '[]
+    'Bodyless
+    (Html ())
+  FormTestPost :: Route
+    '[]
+    '[]
+    ('Body B.ByteString)
+    (Html ())
+
+bodyAny :: BodyCodec B.ByteString
+bodyAny = BodyCodec
+  (pure "*/*")
+  (BL.fromStrict)
+  (Right . BL.toStrict)
+
+bodyText :: BodyCodec ByteString
+bodyText = BodyCodec 
+  (pure "text/html; charset=utf-8") -- ^ NonEmpty list of the HTTP media type names.
+  id -- ^ encode from ByteString to ByteString
+  Right -- ^ decode from ByteString to (Either Text ByteString)
+
+bodyHtml :: BodyCodec (Html a)
+bodyHtml = BodyCodec (pure "text/html;charset=utf-8") renderBS (const (Left "can not decode html"))
+
+bytestring :: CaptureCodec ByteString
+bytestring = CaptureCodec (TE.decodeUtf8 . BL.toStrict) (Just . BL.fromStrict . TE.encodeUtf8)
+
+text :: CaptureCodec Text
+text = CaptureCodec id Just
+
+showReadCodec :: Show a => Read a => CaptureCodec a
+showReadCodec = CaptureCodec tshow (Text.Read.readMaybe . T.unpack)
+
+int :: CaptureCodec Int
+int = showReadCodec
+
+-- | metadata about our routes: value level functions and data for constructing
+--   and decoding paths
+meta :: Route captures queries request response -> MetaCodec captures queries request response
+meta route = case route of
+  HelloWorld -> Meta 
+    (capture bytestring ./ end) -- ^ match "/hello"
+    (optional "b" bytestring .& qend) -- ^ no query parameters
+    bodyless -- ^ no request body
+    (resp (one bodyText)) -- ^ response body is one BodyCodec: our bodyText function above
+    Method.get -- ^ http method: GET
+  FormTest -> Meta
+    (match "test" ./ end)
+    (qend)
+    bodyless
+    (resp (one bodyHtml))
+    Method.get
+  FormTestPost -> Meta
+    (match "test" ./ match "post" ./ end)
+    (qend)
+    (body (one bodyAny))
+    (resp (one bodyHtml))
+    Method.post
+
+-- | this function defines how we handle routes with our web server:
+--   what actions we perform based on the route and its captures & queries
+routes
+  :: forall captures queries request response.
+     Route captures queries request response -- ^ our route GADT, polymorphic over its type variables
+  -> Rec Identity captures -- ^ an extensible record of the captures for this route
+  -> Rec Parameter queries -- ^ an extensible record of the captures for this route
+  -> RequestBody Identity request -- ^ the request body
+  -> TrasaT IO response -- ^ our response
+routes route captures queries reqBody = case route of
+  HelloWorld -> go helloWorld
+  FormTest -> go formTest
+  FormTestPost -> go formTestPost
+  where
+  -- | this helper function uses the `handler` function to unwrap the `Arguments` type family.
+  go :: Arguments captures queries request (TrasaT IO response) -> TrasaT IO response
+  go f = handler captures queries reqBody f
+
+helloWorld :: ByteString -> Maybe ByteString -> TrasaT IO ByteString
+helloWorld a (Just b) = pure $ a <> ", " <> b <> "!"
+helloWorld a Nothing = pure a
+
+type family QueryArguments (querys :: [Param]) (result :: Type) :: Type where
+  QueryArguments '[] r = r
+  QueryArguments (q ': qs) r = ParamBase q -> QueryArguments qs r
+
+-- formArgs :: Monad f => Rec Parameter qrys -> QueryArguments qrys (f (Rec Parameter qrys))
+-- formArgs queries args = do
+--   pure RecNil
+
+formFoo :: TrasaSimpleForm Foo
+formFoo = childErrorList ++> ( Foo 
+  <$> label "Int Field 1" "int1" 
+      ++> setAttr [class_ "input"] (inputInt readInt "int1" 0)
+  <*> label "Bool Field 1" "bool1"
+      ++> inputYesNo "bool1"
+  <*> label "Int Field 2" "in2" 
+      ++> inputInt readInt "int2" 0
+  <*  buttonSubmit (const (Right T.empty)) "" "" ("Submit" :: Text)
+  )
+
+prepare :: Route captures query request response -> Arguments captures query request (Prepared Route response)
+prepare = prepareWith meta
+
+instance IsRoute Route where
+  metaF = meta
+
+formTest :: TrasaT IO (Html ())
+formTest = do
+  (res, html) <- simpleReformGET (encodeRoute $ conceal (prepare FormTest)) formFoo
+  defaultLayout $ do
+    case res of
+      Ok x -> do
+        toHtml $ show x
+        br_ []
+      Error xs -> do
+        toHtml $ show xs
+        br_ []
+    html
+
+formTestPost :: B.ByteString -> TrasaT IO (Html ())
+formTestPost _ = do
+  pure $ pure ()
+
+defaultLayout :: Html () -> TrasaT IO (Html ())
+defaultLayout children = do
+  pure $ html_ $ do
+    head_ $ do
+      link_ [rel_ "stylesheet", href_ "https://unpkg.com/sakura.css/css/sakura.css", type_ "text/css"]
+    body_ $ do
+      children
+
+-- | We define a list of all the routes for our server for wai
+allRoutes :: [Constructed Route]
+allRoutes = [Constructed HelloWorld, Constructed FormTest, Constructed FormTestPost]
+
+-- | Another implimentaiton detail: this creates the data structure used to do routing
+router :: Router Route
+router = routerWith (mapMeta captureDecoding captureDecoding id id . meta) allRoutes
+
+-- | `wai` application
+application :: Application
+application = serveWith
+  (metaCodecToMetaServer . meta) -- ^ implimentaiton detail: this just marshals some types
+  routes -- ^ routes function defined above
+  router -- ^ router function defined above
+
+main :: IO ()
+main = run 8080 (logStdoutDev application)
+
+inputYesNo :: String -> TrasaSimpleForm Bool
+inputYesNo s = mapView 
+  (\x -> label_ [for_ (T.pack s)] $ x *> "Enabled")
+  (inputCheckbox False s)
+
diff --git a/src/Trasa/Form.hs b/src/Trasa/Form.hs
new file mode 100644
--- /dev/null
+++ b/src/Trasa/Form.hs
@@ -0,0 +1,152 @@
+{-# language ConstraintKinds #-}
+{-# language OverloadedStrings #-}
+{-# language TypeFamilies #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Trasa.Form 
+  ( reform
+  , reformQP
+  , reformPost
+  , liftParser
+  , TrasaForm
+  , TrasaSimpleForm
+  , FormError(..)
+  )
+  where
+
+import Control.Monad.Except
+import Control.Monad.Reader
+import Data.Text (Text)
+import Ditto.Backend
+import Ditto.Core hiding (view)
+import Ditto.Result
+import Lucid
+import Trasa.Core hiding (optional)
+import Trasa.Server
+import Trasa.Url
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BSL
+import qualified Data.HashMap.Strict as HM
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import qualified Web.FormUrlEncoded as HTTP
+
+instance FormError Text where
+  type ErrorInputType Text = Text
+  commonFormError = T.pack . (commonFormErrorStr T.unpack)
+
+liftParser :: (Text -> Either Text a) -> (QueryParam -> Either Text a)
+liftParser f q = case q of
+  QueryParamSingle x -> f x
+  QueryParamList [x] -> f x
+  QueryParamFlag -> Left "Unexpected query flag"
+  QueryParamList [] -> Left "Unexpect empty query list"
+  QueryParamList (_:_:_) -> Left "Unexpected query string list"
+
+tshow :: Show a => a -> Text
+tshow = T.pack . show
+
+type TrasaSimpleForm a = Form (TrasaT IO) Text Text (Html ()) a
+type TrasaForm a = Form (TrasaT IO) QueryParam Text (Html ()) a
+
+reform :: (MonadIO m, Monoid view)  
+  => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
+  -> Text -- ^ form name prefix
+  -> Form (TrasaT m) Text err view a  -- ^ the formlet
+  -> TrasaT m (Result err a, view)
+reform toForm prefix formlet = do 
+  reformSingle toForm' prefix formlet
+  where
+  toForm' hidden view = toForm (("formname",prefix) : hidden) view
+
+reformSingle :: (MonadIO m, Monoid view)
+  => ([(Text, Text)] -> view -> view)
+  -> Text
+  -> Form (TrasaT m) Text err view a
+  -> TrasaT m (Result err a, view)
+reformSingle toForm prefix formlet = do
+  (View viewf, res') <- runForm (Environment env) (TL.fromStrict prefix) formlet
+  res <- res'
+  case res of
+    Error errs -> pure (Error errs, toForm [] $ viewf errs)
+    Ok (Proved _ unProved') -> pure (Ok unProved', toForm [] $ viewf [])
+  where
+  env :: MonadIO m => FormId -> TrasaT m (Value Text)
+  env formId = do
+    QueryString queryString <- trasaQueryString <$> ask
+    let val = HM.lookup (tshow formId) queryString
+    case val of
+      Nothing -> pure Missing
+      Just QueryParamFlag -> pure Default -- ???
+      Just (QueryParamSingle x) -> pure (Found x)
+      Just (QueryParamList x) -> pure (Found $ T.intercalate ", " x) -- ???
+
+reformQP :: (MonadIO m, Monoid view)  
+  => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
+  -> Text -- ^ form name prefix
+  -> Form (TrasaT m) QueryParam err view a  -- ^ the formlet
+  -> TrasaT m (Result err a, view)
+reformQP toForm prefix formlet = do 
+  reformSingleQP toForm' prefix formlet
+  where
+  toForm' hidden view = toForm (("formname",prefix) : hidden) view
+
+reformSingleQP :: (MonadIO m, Monoid view)
+  => ([(Text, Text)] -> view -> view)
+  -> Text
+  -> Form (TrasaT m) QueryParam err view a
+  -> TrasaT m (Result err a, view)
+reformSingleQP toForm prefix formlet = do
+  (View viewf, res') <- runForm (Environment env) (TL.fromStrict prefix) formlet
+  res <- res'
+  case res of
+    Error errs -> pure (Error errs, toForm [] $ viewf errs)
+    Ok (Proved _ unProved') -> pure (Ok unProved', toForm [] $ viewf [])
+  where
+  env :: MonadIO m => FormId -> TrasaT m (Value QueryParam)
+  env formId = do
+    QueryString queryString <- trasaQueryString <$> ask
+    let val = HM.lookup (tshow formId) queryString
+    case val of
+      Nothing -> pure Missing
+      Just x -> pure (Found x)
+
+reformPost :: (MonadIO m, Monoid view)  
+  => ([(Text, Text)] -> view -> view) -- ^ wrap raw form html inside a <form> tag
+  -> Text -- ^ form name prefix
+  -> BS.ByteString
+  -> Form (TrasaT m) QueryParam err view a  -- ^ the formlet
+  -> TrasaT m (Result err a, view)
+reformPost toForm prefix reqBody formlet = do 
+  reformSinglePost toForm' prefix reqBody formlet
+  where
+  toForm' hidden view = toForm (("formname",prefix) : hidden) view
+
+reformSinglePost :: (MonadIO m, Monoid view)
+  => ([(Text, Text)] -> view -> view)
+  -> Text
+  -> BS.ByteString
+  -> Form (TrasaT m) QueryParam err view a
+  -> TrasaT m (Result err a, view)
+reformSinglePost toForm prefix reqBody formlet = do
+  let formData = parseRequestBody reqBody
+  (View viewf, res') <- runForm (Environment $ env formData) (TL.fromStrict prefix) formlet
+  res <- res'
+  case res of
+    Error errs -> pure (Error errs, toForm [] $ viewf errs)
+    Ok (Proved _ unProved') -> pure (Ok unProved', toForm [] $ viewf [])
+  where
+  env :: MonadIO m => HM.HashMap Text [Text] -> FormId -> TrasaT m (Value QueryParam)
+  env multipart formId = do
+    let val = HM.lookup (tshow formId) multipart
+    case val of
+      Nothing -> pure Missing
+      Just [] -> pure $ Found QueryParamFlag
+      Just [x] -> pure $ Found $ QueryParamSingle x
+      Just xs -> pure $ Found $ QueryParamList xs
+
+parseRequestBody :: BS.ByteString -> HM.HashMap Text [Text]
+parseRequestBody reqBody = case HTTP.urlDecodeForm (BSL.fromStrict reqBody) of
+  Left _ -> HM.empty
+  Right (HTTP.Form formData) -> formData
diff --git a/src/Trasa/Form/Lucid.hs b/src/Trasa/Form/Lucid.hs
new file mode 100644
--- /dev/null
+++ b/src/Trasa/Form/Lucid.hs
@@ -0,0 +1,39 @@
+{-# language OverloadedStrings #-}
+
+module Trasa.Form.Lucid where
+
+import Control.Monad.Except
+import Data.Text (Text)
+import Ditto.Core hiding (view)
+import Ditto.Lucid
+import Ditto.Result
+import Lucid
+import Trasa.Form
+import Trasa.Server
+import Trasa.Url
+import qualified Data.ByteString as BS
+
+queryParamReformGET :: (MonadIO m, Show b, Applicative f) 
+  => Text
+  -> Form (TrasaT m) QueryParam err (HtmlT f ()) b 
+  -> TrasaT m (Result err b, HtmlT f ())
+queryParamReformGET action = reformQP (formGenGET' action) "reform"
+
+simpleReformGET :: (MonadIO m, Show b, Applicative f) 
+  => Text
+  -> Form (TrasaT m) Text err (HtmlT f ()) b 
+  -> TrasaT m (Result err b, HtmlT f ())
+simpleReformGET action form = reform (formGenGET' action) "reform" form
+
+queryParamReformPOST :: (MonadIO m, Show b, Applicative f) 
+  => Text
+  -> BS.ByteString
+  -> Form (TrasaT m) QueryParam err (HtmlT f ()) b 
+  -> TrasaT m (Result err b, HtmlT f ())
+queryParamReformPOST action reqBody form = reformPost (formGenPOST' action) "reform" reqBody form
+
+formGenGET' :: Applicative f => Text -> [(Text, Text)] -> HtmlT f b -> HtmlT f b
+formGenGET' url = formGenGET url
+
+formGenPOST' :: Applicative f => Text -> [(Text, Text)] -> HtmlT f b -> HtmlT f b
+formGenPOST' url = formGenPOST url
diff --git a/trasa-form.cabal b/trasa-form.cabal
new file mode 100644
--- /dev/null
+++ b/trasa-form.cabal
@@ -0,0 +1,65 @@
+cabal-version: 2.0
+name: trasa-form
+version: 0.1.0.0
+synopsis: generate forms using lucid, ditto and trasa
+description: Formlets library for trasa using ditto as its backend.
+             Although trasa already has machinery for creating
+             typesafe forms, this library with ditto allow a more
+             composable approach to form generation/validation.
+license: BSD3
+license-file: LICENSE
+author: goolord
+maintainer: zacharyachurchill@gmail.com
+category: Web
+build-type: Simple
+extra-source-files: CHANGELOG.md
+
+source-repository head
+    type: git
+    location: https://github.com/goolord/ditto.git
+
+library
+  exposed-modules:
+    Trasa.Form
+    Trasa.Form.Lucid
+  -- other-modules:
+  -- other-extensions:
+  ghc-options: -Wall
+  build-depends: 
+      base
+    , http-api-data
+    , cookie
+    , http-types
+    , lucid
+    , mtl
+    , quantification
+    , ditto >= 0.1.2.0 && < 0.2
+    , ditto-lucid >= 0.1.2.0 && < 0.2
+    , text
+    , trasa
+    , trasa-server >= 0.5.3
+    , unordered-containers
+    , bytestring
+  hs-source-dirs: src
+  default-language: Haskell2010
+executable test-server
+  ghc-options: -Wall
+  main-is: Main.hs
+  build-depends: 
+       base ^>=4.12.0.0
+     , bytestring
+     , lucid
+     , quantification
+     , ditto >= 0.1.2.0 && < 0.2
+     , ditto-lucid >= 0.1.2.0 && < 0.2
+     , text
+     , trasa == 0.4.*
+     , trasa-extra
+     , trasa-form
+     , trasa-server
+     , wai
+     , wai-extra == 3.0.27 
+     , mtl
+     , warp
+  hs-source-dirs: app
+  default-language: Haskell2010
