├── .gitignore ├── demo ├── Setup.hs ├── test │ └── Spec.hs ├── app │ └── Main.hs ├── src │ ├── Route.hs │ ├── App.hs │ └── Handler.hs ├── LICENSE ├── demo.cabal └── stack.yaml ├── docs ├── resources │ ├── conan.gif │ ├── web-framework.png │ └── web-framework.graffle ├── index.html └── SLIDES.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | -------------------------------------------------------------------------------- /demo/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /demo/test/Spec.hs: -------------------------------------------------------------------------------- 1 | main :: IO () 2 | main = putStrLn "Test suite not yet implemented" 3 | -------------------------------------------------------------------------------- /docs/resources/conan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbaatz/build-a-haskell-web-framework/HEAD/docs/resources/conan.gif -------------------------------------------------------------------------------- /docs/resources/web-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbaatz/build-a-haskell-web-framework/HEAD/docs/resources/web-framework.png -------------------------------------------------------------------------------- /docs/resources/web-framework.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cbaatz/build-a-haskell-web-framework/HEAD/docs/resources/web-framework.graffle -------------------------------------------------------------------------------- /demo/app/Main.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import App (app) 4 | 5 | import Network.Wai.Handler.Warp (run) 6 | 7 | main :: IO () 8 | main = run 3000 app 9 | -------------------------------------------------------------------------------- /demo/src/Route.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module Route 5 | ( Route(..) 6 | , parseRoute 7 | ) where 8 | 9 | import BasicPrelude 10 | 11 | import Data.Attoparsec.ByteString.Char8 12 | 13 | data Route = Home | Message Int 14 | 15 | parseRoute :: ByteString -> Either String Route 16 | parseRoute = parseOnly parser 17 | 18 | parser :: Parser Route 19 | parser = choice 20 | [ string "/" <* endOfInput >> return Home 21 | , string "/messages/" >> fmap Message (decimal <* endOfInput) 22 | ] 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Talk: Build yourself a Haskell web framework 2 | ============================================ 3 | 4 | Zurich HaskellerZ meetup -- 27 October 2016 5 | 6 | Slides were created using [remark](https://github.com/gnab/remark) and can be viewed online at https://cbaatz.github.io/build-a-haskell-web-framework/. 7 | 8 | To run the code from the live-coding session: 9 | 10 | 1. Install [Stack](www.haskellstack.org/). 11 | 2. Got to the `demo/` folder in this repo. 12 | 3. `stack ghci` then type `:main` at the prompt. This starts the web-server. Press Ctrl-C to stop it. 13 | 14 | Please note that the code form the live-coding session is not polished, merely a proof-of-concept. 15 | -------------------------------------------------------------------------------- /demo/src/App.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE NoImplicitPrelude #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module App 5 | ( app 6 | ) where 7 | 8 | import BasicPrelude 9 | 10 | import Control.Monad.Reader (asks) 11 | import qualified Data.ByteString.Lazy as L 12 | import qualified Data.ByteString.Lazy.Char8 as L8 13 | import Network.HTTP.Types 14 | import Network.Wai 15 | 16 | import Handler 17 | import Route 18 | 19 | app :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived 20 | app req respond = do 21 | let methodEither = parseMethod (requestMethod req) 22 | routeEither = parseRoute (rawPathInfo req) 23 | env = Env req 24 | case (methodEither, routeEither) of 25 | (Right method, Right route) -> do 26 | runHandler (router method route) env >>= respond 27 | _ -> runHandler notFound env >>= respond 28 | 29 | router :: StdMethod -> Route -> Handler L.ByteString 30 | router GET Home = do 31 | req <- asks envRequest 32 | setStatus status200 33 | return (L8.pack (show $ remoteHost req)) 34 | router GET (Message i) = do 35 | when (i == 3) (redirect "/") 36 | return (L8.pack ("Message #" <> (show i))) 37 | router _ _ = notFound 38 | 39 | notFound :: Handler L.ByteString 40 | notFound = do 41 | addHeader ("Content-type", "text/html") 42 | setStatus status404 43 | return $ "

404 NOT FOUND

" 44 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Build yourself a Haskell web framework 5 | 6 | 21 | 22 | 23 | 25 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Carl Baatz (c) 2016 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Carl Baatz nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /demo/demo.cabal: -------------------------------------------------------------------------------- 1 | name: demo 2 | version: 0.1.0 3 | synopsis: Code from live-coding during talk (not polished) 4 | description: Please see README.md 5 | homepage: https://github.com/cbaatz/build-a-haskell-web-framework#readme 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Carl Baatz 9 | maintainer: carl.baatz@gmail.com 10 | copyright: 2016 Carl Baatz 11 | category: Web 12 | build-type: Simple 13 | cabal-version: >=1.10 14 | 15 | library 16 | hs-source-dirs: src 17 | exposed-modules: App 18 | , Route 19 | , Handler 20 | build-depends: base >= 4.7 && < 5 21 | , basic-prelude 22 | , wai 23 | , http-types 24 | , attoparsec 25 | , bytestring 26 | , mtl 27 | default-language: Haskell2010 28 | 29 | executable demo-exe 30 | hs-source-dirs: app 31 | main-is: Main.hs 32 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 33 | build-depends: base 34 | , demo 35 | , warp 36 | default-language: Haskell2010 37 | 38 | test-suite demo-test 39 | type: exitcode-stdio-1.0 40 | hs-source-dirs: test 41 | main-is: Spec.hs 42 | build-depends: base 43 | , demo 44 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 45 | default-language: Haskell2010 46 | 47 | source-repository head 48 | type: git 49 | location: https://github.com/cbaatz/build-a-haskell-web-framework 50 | -------------------------------------------------------------------------------- /demo/src/Handler.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 2 | {-# LANGUAGE NoImplicitPrelude #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | module Handler 6 | ( Handler 7 | , runHandler 8 | , Env(..) 9 | , addHeader 10 | , setStatus 11 | , redirect 12 | ) where 13 | 14 | import BasicPrelude 15 | 16 | import Control.Monad.Except 17 | import Control.Monad.Reader 18 | import Control.Monad.State 19 | import qualified Data.ByteString.Lazy as L 20 | import Network.HTTP.Types 21 | import Network.Wai 22 | 23 | data Env = Env { envRequest :: Request } 24 | data Meta = Meta { metaHeaders :: [Header], metaStatus :: Maybe Status } 25 | data Bail = BailNotFound | BailRedirect ByteString 26 | 27 | newtype Handler a = Handler (StateT Meta (ExceptT Bail (ReaderT Env IO)) a) 28 | deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env, MonadState Meta, MonadError Bail) 29 | 30 | runHandler :: Handler L.ByteString -> Env -> IO Response 31 | runHandler (Handler stateT) env = do 32 | let exceptT = runStateT stateT (Meta [] Nothing) 33 | readerT = runExceptT exceptT 34 | resultEither <- runReaderT readerT env 35 | case resultEither of 36 | Right (body, meta) -> do 37 | let headers = metaHeaders meta 38 | status = maybe status200 id (metaStatus meta) 39 | return $ responseLBS status headers body 40 | Left BailNotFound -> do 41 | return $ responseLBS status404 [] "404 NOT FOUND" 42 | Left (BailRedirect url) -> do 43 | return $ responseLBS status307 [("Location", url)] "" 44 | 45 | redirect :: ByteString -> Handler () 46 | redirect url = throwError (BailRedirect url) 47 | 48 | addHeader :: Header -> Handler () 49 | addHeader header = modify $ \meta -> meta { metaHeaders = metaHeaders meta <> [header] } 50 | 51 | setStatus :: Status -> Handler () 52 | setStatus status = modify $ \meta -> meta { metaStatus = Just status } 53 | -------------------------------------------------------------------------------- /demo/stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # resolver: 16 | # name: custom-snapshot 17 | # location: "./custom-snapshot.yaml" 18 | resolver: lts-7.5 19 | 20 | # User packages to be built. 21 | # Various formats can be used as shown in the example below. 22 | # 23 | # packages: 24 | # - some-directory 25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 26 | # - location: 27 | # git: https://github.com/commercialhaskell/stack.git 28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 30 | # extra-dep: true 31 | # subdirs: 32 | # - auto-update 33 | # - wai 34 | # 35 | # A package marked 'extra-dep: true' will only be built if demanded by a 36 | # non-dependency (i.e. a user package), and its test suites and benchmarks 37 | # will not be run. This is useful for tweaking upstream packages. 38 | packages: 39 | - '.' 40 | # Dependency packages to be pulled from upstream that are not in the resolver 41 | # (e.g., acme-missiles-0.3) 42 | extra-deps: [] 43 | 44 | # Override default flag values for local packages and extra-deps 45 | flags: {} 46 | 47 | # Extra package databases containing global packages 48 | extra-package-dbs: [] 49 | 50 | # Control whether we use the GHC we find on the path 51 | # system-ghc: true 52 | # 53 | # Require a specific version of stack, using version ranges 54 | # require-stack-version: -any # Default 55 | # require-stack-version: ">=1.2" 56 | # 57 | # Override the architecture used by stack, especially useful on Windows 58 | # arch: i386 59 | # arch: x86_64 60 | # 61 | # Extra directories used by stack for building 62 | # extra-include-dirs: [/path/to/dir] 63 | # extra-lib-dirs: [/path/to/dir] 64 | # 65 | # Allow a newer minor version of GHC than the snapshot specifies 66 | # compiler-check: newer-minor -------------------------------------------------------------------------------- /docs/SLIDES.md: -------------------------------------------------------------------------------- 1 | layout: true 2 | name: implementation 3 | class: center, middle 4 | 5 | ## [Implementation demo] 6 | 7 | --- 8 | layout: false 9 | class: center, middle 10 | 11 |

Build yourself a
Haskell web framework

12 | 13 | HaskellerZ meetup — 27 October 2016
14 | Carl Baatz 15 | 16 | https://github.com/cbaatz/build-a-haskell-web-framework 17 | 18 | --- 19 | 20 | ## Objectives 21 | 22 | Web frameworks — Yesod, Snap, Scotty, or Spock for example — help us implement web servers. But what goes on behind the scenes of such a framework? We'll try to understand that better today by writing our own super simple Haskell web framework. 23 | 24 | ## Prerequisites 25 | 26 | - Familiarity with Haskell syntax. 27 | - Familiarity with monads. 28 | - Intended to be beginner friendly. 29 | 30 | ## Ask questions if you're confused! 31 | 32 | ??? 33 | 34 | - Build a mental model of a web framework. 35 | - We'll build a simple web framwork. 36 | - Not mature or feature complete. 37 | - Not a statement on how frameworks *should* be built. 38 | - Experience with monads? 39 | - A good example of how useful custom monads can be. 40 | - Beginner friendly. 41 | - Ask questions! 42 | 43 | --- 44 | class: center, middle 45 | 46 | # A bit of background. 47 | 48 | --- 49 | layout: true 50 | name: web-server 51 | 52 | # What is a web server? 53 | 54 | A *web server* accepts **HTTP requests** and returns **HTTP responses**. 55 | --- 56 | --- 57 | 58 | ### HTTP request 59 | 60 | ```plain_text 61 | POST /sign-in/ HTTP/1.1 62 | Host: example.com 63 | Content-Length: 45 64 | Cache-Control: no-cache 65 | User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36 66 | Content-Type: application/x-www-form-urlencoded 67 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 68 | 69 | identifier=username&password=mypassword 70 | ``` 71 | 72 | ??? 73 | 74 | - Method 75 | - Path (URL) 76 | - Headers 77 | - Body 78 | 79 | --- 80 | 81 | ### HTTP response 82 | 83 | ```plain_text 84 | HTTP/1.1 401 NOT AUTHORIZED 85 | Server: nginx/1.6.2 86 | Date: Sun, 23 Oct 2016 20:43:31 GMT 87 | Content-Type: text/html; charset=utf-8 88 | 89 |

Not authorized

90 | ``` 91 | 92 | ??? 93 | 94 | - Status code 95 | - Headers 96 | - Body 97 | 98 | --- 99 | layout: false 100 | 101 | # What is a web framework? 102 | 103 | A web framework sits on top of some web server and provides developers with structure and tools to make it easier to implement a web server application. 104 | 105 | ![Stack diagram](resources/web-framework.png) 106 | 107 | ??? 108 | 109 | - Web framework provides structure and helper functions. 110 | - Application logic hooks into web framwork. 111 | 112 | --- 113 | # What do frameworks do? 114 | 115 | Functionality varies widely between frameworks; some features on offer might be: 116 | 117 | - **Routing**: divide your code by request method and URL. 118 | - **Request/response access**: DSL for reading requests and manipulating responses. 119 | - **Common tasks**: Redirection, content type setting, link generation. 120 | - **Templating**: HTML or CSS templating. 121 | - **Security**: Guard against XSS, CSRF, and SQL-injection. 122 | - **Data models**: Tools for accessing your data 123 | - **Sessions**: Track identity between requests. 124 | - **Codebase structure**: Specify file naming and organisation. 125 | - ... 126 | 127 | In the interest of time and clarity our framework will have only a few of these features. 128 | 129 | ??? 130 | 131 | - What else do you want from a framework? 132 | - Feature sets vary widely. 133 | - Framework vs libraries. 134 | - Libraries more flexible, but require more knowledge and glue code. 135 | - Our framework will have minimal features. 136 | - Hopefully it should be clear how to add more features on top however. 137 | 138 | --- 139 | class: center, middle 140 | 141 | # Let's get started. 142 | 143 | --- 144 | 145 | # A Haskell web server 146 | 147 | The `wai` package gives us an API for writing web server applications against HTTP `Request` and `Response` types in `Network.Wai`. 148 | 149 | ```haskell 150 | data Request -- Abstract 151 | data Response -- Abstract 152 | type Application :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived 153 | 154 | app :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived 155 | app req respond = respond (responseLBS status200 [] "Hello, world!") 156 | ``` 157 | 158 | `Application` is in continuation passing style — we get a request and a continuation function which we call with our response. 159 | 160 | ??? 161 | 162 | - WAI = Web Application Interface 163 | - Most of the newer frameworks use WAI 164 | 165 | --- 166 | 167 | # A Haskell web server 168 | 169 | The `warp` package implements a web server that can serve applications written to the WAI API. `run` in `Network.Wai.Handler.Warp` will: 170 | 171 | - Bind to a port (3000 in the example below) 172 | - Parse incoming requests into WAI `Request`s 173 | - Serialize and return the application's WAI `Response`s 174 | 175 | ```haskell 176 | run :: Port -> Application -> IO () 177 | 178 | main :: IO () 179 | main = run 3000 app 180 | ``` 181 | 182 | ??? 183 | 184 | - There are other backends to WAI like CGI and FastCGI, but Warp is a common choice. 185 | 186 | --- 187 | 188 | template: implementation 189 | 190 | A "Hello, world!" web server. 191 | 192 | --- 193 | name: routing 194 | 195 | # Routing 196 | 197 | The first thing we usually want to do when a request comes in is to *route* it to an appropriate *handler* based on the request method and path. Existing frameworks provide a variety of solutions for this. 198 | 199 | ??? 200 | 201 | - We could write an arbitrarily complex web application based on this. 202 | - Working with "raw" `Request`s and `Response`s is awkward. 203 | - Routing is often the first thing we want to do. 204 | 205 | --- 206 | template: routing 207 | 208 | ## Yesod 209 | 210 | ```haskell 211 | mkYesod "Piggies" [parseRoutes| 212 | / HomeR GET 213 | /about AboutR GET 214 | |] 215 | ``` 216 | 217 | Yesod uses Template Haskell to provide a DSL for the routing table. 218 | 219 | --- 220 | template: routing 221 | 222 | ## Spock 223 | 224 | ```haskell 225 | site = do 226 | get "/" $ 227 | html "Calculate 313 + 3" 228 | get ("hello" ":name") $ do 229 | name <- param' "name" 230 | text $ "Hello " <> name <> "!" 231 | get ("calculator" ":a" "+" ":b") $ do 232 | a <- param' "a" 233 | b <- param' "b" 234 | text $ pack $ show (a + b :: Int) 235 | ``` 236 | 237 | --- 238 | template: routing 239 | 240 | ## Snap 241 | 242 | ```haskell 243 | site = 244 | ifTop (writeBS "hello world") <|> 245 | route [ ("foo", writeBS "bar") 246 | , ("echo/:echoparam", echoHandler) 247 | ] <|> 248 | dir "static" (serveDirectory ".") 249 | ``` 250 | --- 251 | 252 | # Routing 253 | 254 | To keep things simple, we'll use an `attoparsec` parser and normal function application to route requests: 255 | 256 | ```haskell 257 | data Route = Home | Message Int 258 | 259 | parser :: Parser Route 260 | parser = choice 261 | [ string "/" <* endOfInput >> return Home 262 | , string "/messages/" >> fmap Message (decimal <* endOfInput) 263 | ] 264 | 265 | router :: Method -> Route -> IO Response 266 | router GET Home = homeHandler 267 | router GET (Message messageId) = messageHandler messageId 268 | router _ _ = return notFound 269 | 270 | notFound :: Response 271 | ``` 272 | 273 | (Wait a minute, where's the request? We'll get to that.) 274 | 275 | ??? 276 | 277 | - `Route` is an ADT for "locations" in our application. 278 | - `Method` is the HTTP method. 279 | - There are several approaches to parsing paths. 280 | - We'll use an `attoparsec` parser. Less pretty, but simple, powerful, and a useful general skill. 281 | - We'll want a pretty printer as well later. Need to be kept in sync. 282 | - Can test pretty printer works. Parser can be more lenient however. 283 | 284 | --- 285 | template: implementation 286 | 287 | Routing functionality. 288 | 289 | ??? 290 | --- 291 | 292 | # Handlers 293 | 294 | Now that we can route requests to handlers we ask ourselves: how should we write our handlers? We know we'll want to do things like: 295 | 296 | - Load data from the database 297 | - Log information 298 | - Read headers and cookies 299 | - Read query parameters 300 | - Check authorization 301 | - Construct absolute URLs 302 | - ... 303 | 304 | To do that we need access to things like the request, configuration, and DB and logger handles (IO functions that access the DB or write log messages). 305 | 306 | --- 307 | layout: true 308 | 309 | # The `Handler` monad 310 | 311 | We could choose to pass these values around explicitly or to bundle them up in a Reader monad. To reduce glue-code and allow us to add other useful functionality later, we're going to go with a `Handler` monad. 312 | 313 | --- 314 | 315 | ## Explicit arguments 316 | 317 | ```haskell 318 | myHandler :: Config -> Request -> DBHandle -> LogHandle -> IO Response 319 | myHandler config req dbHandle logHandle = do 320 | log logHandle Info "Reached myHandler" 321 | userIdMay <- getLoggedInUser req dbHandle logHandle 322 | case userIdMay of 323 | Nothing -> return notFound 324 | Just userId -> do 325 | userInfoMay <- fetchUserInfo dbHandle logHandle userId 326 | return $ maybe notFound renderUserInfo userInfoMay 327 | ``` 328 | 329 | --- 330 | 331 | ## Reader monad 332 | 333 | ```haskell 334 | myHandler :: Handler Response 335 | myHandler = do 336 | log "Reached myHandler" 337 | userIdMay <- getLoggedInUser 338 | case userIdMay of 339 | Nothing -> return notFound 340 | Just userId -> do 341 | userInfoMay <- fetchUserInfo 342 | return $ maybe notFound renderUserInfo userInfoMay 343 | ``` 344 | 345 | --- 346 | template: implementation 347 | layout: false 348 | 349 | A `Handler` reader monad with the `Request` in the environment. 350 | 351 | --- 352 | layout: false 353 | 354 | # Setting headers and status 355 | 356 | Now we move on to actually produce the response and realise that it's a bit awkward to manage headers: 357 | 358 | ```haskell 359 | home :: Handler Response 360 | home = do 361 | let cacheHeader = ("Cache-control", "no-cache") 362 | userIdMay <- getLoggedInUser 363 | (body, headers, status) <- case userIdMay of 364 | Just userId -> homeForUser userId 365 | Nothing -> publicHome 366 | return (responseLBS status ([cacheHeader] <> headers) body) 367 | ``` 368 | 369 | Here we manually construct a list of headers that we finally include in the response. Having to explicitly name and merge headers as we split up our handlers is annoying. 370 | 371 | --- 372 | 373 | # Setting headers and status 374 | 375 | What if we hide the response headers in the `Handler` monad and create functions to add and change them? Then we can do something like this: 376 | 377 | ```haskell 378 | home :: Handler Body 379 | home = do 380 | addHeader "Cache-control" "no-cache" 381 | userIdMay <- getLoggedInUser 382 | case userIdMay of 383 | Just userId -> homeForUser userId 384 | Nothing -> publicHome 385 | ``` 386 | 387 | --- 388 | template: implementation 389 | 390 | Add state with headers and status to the `Handler` monad. 391 | 392 | ??? 393 | 394 | - We'll also put the status in there since that's convenient. 395 | - We can set status at the most appropriate point in our handler, not just at the end. 396 | - We can return just the body from our handler. 397 | - We could also put the body in the state. 398 | - Requiring our handlers to return a body add some type safety. 399 | 400 | --- 401 | 402 | # Exceptions and failure 403 | 404 | Our `Handler` monad is starting to feel a bit like a framework, but one key features is missing. What happens when it's not all rainbows and unicorns? Let's say we need to write a handler for a restricted part of our website: 405 | 406 | ```haskell 407 | restricted :: MessageId -> Handler L.ByteString 408 | restricted messageId = do 409 | messageMay <- fetchMessage messageId 410 | case messageMay of 411 | Nothing -> notFound 412 | Just message -> do 413 | userIdMay <- getLoggedInUserId 414 | case userIdMay of 415 | Nothing -> notFound 416 | Just userId -> if messageOwnerId message == userId 417 | then renderMessage message 418 | else notFound 419 | ``` 420 | 421 | --- 422 | class: center, middle 423 | .small[![Ew](resources/conan.gif)] 424 | --- 425 | 426 | # Exceptions and failure 427 | 428 | Not nice. We want to tell our functions: *give me what I want, or don't bother returning*. 429 | 430 | Well, there's a monad for that: the `Except` monad. If we add that functionality to our `Handler` monad we can bail whenever we want and rewrite our handler like this: 431 | 432 | ```haskell 433 | restricted :: MessageId -> Handler L.ByteString 434 | restricted messageId = do 435 | message <- requireMessage messageId 436 | userId <- requireUserId 437 | notFoundWhen (messageOwnerId message != userId) 438 | renderMessage message 439 | ``` 440 | 441 | Nice. 442 | 443 | ??? 444 | 445 | - Could tidy up the previous version with `LambdaCase`, but structure remains. 446 | - Functions now have a concept of success and failure. 447 | - If they fail, the rest of the handler is skipped. 448 | 449 | --- 450 | template: implementation 451 | 452 | Add failure functionality to the `Handler` monad. 453 | --- 454 | 455 | # Expanding on the core 456 | 457 | We've now laid the foundation for a framework. It's lacking polish and features, but the basic structure is there: routing, environment access, response manipulation, and app level exception handling. If we were to expand on this core the next steps might be: 458 | 459 | - More helpers (`permanentRedirect`, `temporaryRedirect`, `removeHeader`, `setHeader`, `setCookie`, `removeCookie`, etc.). 460 | - DB and logger handles in the `Handler` environment. 461 | - Link generation. 462 | - Automatic redirection to canonical URLs. 463 | - Automatic selection of appropriate response representation. 464 | - Form handling and CSRF protection. 465 | - Configuration options (auto-append slashes, log level, etc.) 466 | - ... 467 | 468 | --- 469 | class: middle, center 470 | 471 | # Questions? 472 | --- 473 | class: middle, center 474 | 475 | # Discussion 476 | 477 | What do you want out of a web framework? 478 | 479 | Which frameworks do you prefer? 480 | 481 | How can the Haskell eco-system improve on web development? 482 | 483 | --- 484 | layout: true 485 | 486 | # Appendix: DB access 487 | 488 | To get access to a DB in our handlers, the basic idea is: 489 | 490 | 1. Create a `DBHandle` with IO functions for accessing the DB. 491 | 2. Add the `DBHandle` to the `Handler` environment. 492 | 3. Write a `withDB :: (DBHandle -> IO a) -> Handler a` function. 493 | 4. Initialise a `DBHandle` in the main function. 494 | 5. Pass the `DBHandle` to the app function so we can put it in the environment. 495 | 6. Access the DB via the `withDB` function in handlers. 496 | 497 | --- 498 | 499 | ```haskell 500 | data DBHandle = DBHandle 501 | { dbWrite :: Text -> Text -> IO () 502 | , dbRead :: Text -> IO (Maybe Text) 503 | } 504 | 505 | initDB :: IO DBHandle 506 | ``` 507 | 508 | ```haskell 509 | withDB :: (DBHandle -> IO a) -> Handler a 510 | withDB action = do 511 | dbHandle <- asks envDBHandle 512 | liftIO $ action dbHandle 513 | ``` 514 | --- 515 | 516 | ```haskell 517 | main = do 518 | dbHandle <- initDB 519 | run 3000 (app dbHandle) 520 | ``` 521 | 522 | ```haskell 523 | myHandler :: Int -> Handler () 524 | myHandler i = do 525 | withDB (\db -> dbWrite db "latest" (tshow i)) 526 | ``` 527 | 528 | ??? 529 | 530 | - Create a basic DBHandle with write and read functions in a DB module. 531 | - initDB :: IO DBHandle 532 | - Add DBHandle to Handler env. 533 | - withDB :: (DBHandle -> IO a) -> Handler a 534 | - Initialise DB handle in the main function. 535 | - Use withDB and the DBHandle functions in the handlers. 536 | 537 | --- 538 | layout: false 539 | 540 | # Appendix: Link generation 541 | 542 | 1. Write a (pure) pretty printer for the `Route` type. 543 | 2. Pass `Route` values to the pretty printer in your templates or wherever you need them. 544 | 545 | Note that the pretty-printer can be tested by running `Route` values through the pretty printer and then the parser checking that you get the same thing back. The parser can be more lenient than that, but that's often OK or even desireable. 546 | 547 | ```haskell 548 | printRoute :: Route -> ByteString 549 | printRoute Home = "/" 550 | printRoute About = "/about/" 551 | printRoute (Message i) = "/messages/" <> (encodeUtf8 $ tshow i) 552 | ``` 553 | 554 | --- 555 | 556 | # Appendix: Canonical URLs 557 | 558 | If we have a pretty printer for `Route`, we can easily redirect to canonical links automatically. 559 | 560 | 1. After parsing the route successfully, check if it's a GET request. 561 | 2. Pretty print the parsed `Route` and check that it matches the raw path in the request. 562 | 3. If they don't match, return a redirect response to the pretty printed route. 563 | 564 | ```haskell 565 | case (methodEither, routeEither) of 566 | (Right method, Right route) -> do 567 | response <- if (printRoute route) == (rawPathInfo req) 568 | then runHandler (router method route) env 569 | else runHandler (throwError $ BailRedirect (printRoute route)) env 570 | respond response 571 | ``` 572 | --- 573 | 574 | # Appendix: Representations 575 | 576 | HTTP requests can ask for a particular representation type — HTML, plain text, JSON, etc.. We can introduce a `Body` type that includes all the representations the handler knows how to produce (which should be efficient because of laziness) and then choose a particular representation in the `runHandler` based on the request's `Accept` header. 577 | 578 | ```haskell 579 | data Body = Body { 580 | { bodyHtml :: Maybe Html 581 | , bodyJSON :: forall j. ToJSON j => Maybe j 582 | , bodyOthers :: [(ByteString, L.ByteString)] 583 | } 584 | ``` 585 | 586 | ```haskell 587 | router :: StdMethod -> Route -> Handler (Maybe Body) 588 | ``` 589 | 590 | Another option would be to put the bodies in the `Handler` state together with the headers and status. That would probably make for neater handlers, but remove the check that handlers conciously return a body (or `Nothing`). 591 | --------------------------------------------------------------------------------