🪁 A simple, type-safe http server for PureScript
Go to file
2022-05-22 19:37:50 +01:00
.github/workflows Use niv out of nixpkgs (#195) 2022-05-04 23:33:37 -07:00
docs Add hello world example to readme & format code 2022-05-22 17:54:11 +01:00
LICENSES Rename modules and rudimentarily update readme 2022-05-22 12:48:32 +01:00
src Make settings optional 2022-05-22 17:37:42 +01:00
test Add hello world example to readme & format code 2022-05-22 17:54:11 +01:00
.envrc #153 bind to 0.0.0.0 instead of localhost (#154) 2020-02-26 16:43:38 -08:00
.gitignore Update for PureScript 0.15 (#194) 2022-05-04 14:02:29 -07:00
.tidyrc.json Update for PureScript 0.15 (#194) 2022-05-04 14:02:29 -07:00
bower.json Update bower.json 2022-05-22 19:37:50 +01:00
Contributing.md Rename 'master' branch to 'main' (#173) 2021-03-22 12:34:49 -07:00
History.md Release notes for v0.14.0 2021-12-06 21:03:07 -08:00
License Rename modules and rudimentarily update readme 2022-05-22 12:48:32 +01:00
packages.dhall Update packages and spago files 2022-05-22 12:34:27 +01:00
Readme.md Add hmr example 2022-05-22 19:29:23 +01:00
Releasing.md Update Releasing notes 2021-11-20 09:47:43 -08:00
shell.nix Use niv out of nixpkgs (#195) 2022-05-04 23:33:37 -07:00
sources.json Update for PureScript 0.15 (#194) 2022-05-04 14:02:29 -07:00
sources.nix Use niv out of nixpkgs (#195) 2022-05-04 23:33:37 -07:00
spago.dhall Fix package meta info 2022-05-22 19:37:14 +01:00
test.dhall Update packages and spago files 2022-05-22 12:34:27 +01:00

HTTPurple 🪁

License

A 🎨 colourful fork of the amazing HTTPure http server framework.

Coming from HTTPure? You might want to have a look at the differences to HTTPure.

ToC

  1. Installation
  2. Quick start
  3. Documenation
  4. Examples
  5. Testing
  6. Differences to HTTPure
  7. License

Installation

spago install httpurple

Quick start

module Main where

import Prelude hiding ((/))

import Data.Generic.Rep (class Generic)
import HTTPurple (ServerM, ok, serve)
import Routing.Duplex (RouteDuplex', root, segment)
import Routing.Duplex.Generic (sum)
import Routing.Duplex.Generic.Syntax ((/))

data Route = Hello String

derive instance Generic Route _

route :: RouteDuplex' Route
route = root $ sum
  { "Hello": "hello" / segment
  }

main :: ServerM
main =
  serve { port: 8080 } { route, router }
  where
  router { route: Hello name } = ok $ "hello " <> name

then start the server

➜ spago run
           Src   Lib   All
Warnings   0     0     0  
Errors     0     0     0  
[info] Build succeeded.
HTTPurple 🪁 up and running on http://0.0.0.0:8080

query your server, e.g. using httpie

➜ http http://localhost:8080/hello/🗺  
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 10
Date: Sun, 22 May 2022 16:50:52 GMT
Keep-Alive: timeout=5

hello 🗺

Documentation

See the docs folder for more in-depth guides.

Examples

HTTPurple ships with a number of examples. To run an example, in the project root, run:

spago -x test.dhall run --main Examples.<Example Name>.Main

Each example's startup banner will include information on routes available on the example server.

Testing

To run the test suite, in the project root run:

spago -x test.dhall test

Differences to HTTPure

HTTPurple 🪁 is a fork of HTTPure that I started to freely experiment with some ideas I have on improving the usage experience. Currently I have no intentions on back-porting any of it to HTTPure, as I don't have the time for it and also don't want to restrict myself.

If you have used HTTPure before, you'll probably want to go through the following changes to get started using HTTPurple 🪁:

Routing-duplex

The most notable difference to HTTPure is that HTTPurple 🪁 uses the amazing routing-duplex library for routing. I found the previous lookup-based routing tedious to work with, especially when having more complex routes, and quite error-prone, especially if you need reverse-routing for redirects.

routing-duplex offers an elegant bidirectional routing which was initially designed for SPAs. Have a look at the really extensive documentation. The benefits of using routing-duplex are

  • Much simpler and less tedious definition of routes
  • Roundtrip printing/parsing of routes, so no more invalid redirects
  • Exhaustive pattern matching so you are sure to match all defined routes
  • Option to separate routes into logical groups

Here is a bit more elaborated examples:

module Main where

import Prelude hiding ((/))

import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Tuple (Tuple(..))
import Effect.Console (log)
import HTTPurple (type (<+>), Request, ResponseM, ServerM, found', headers, ok, notFound, orElse, serve, (<+>))
import Record as Record
import Routing.Duplex (RouteDuplex', as, optional, print, root, segment, string)
import Routing.Duplex.Generic as G
import Routing.Duplex.Generic.Syntax ((/), (?))
import Type.Prelude (Proxy(..))

-- define the ADT representing your Api
data Route
  = Home
  | Profile String
  | Account String
  | Search { q :: String, sorting :: Maybe Sort }

derive instance Generic Route _

data Sort = Asc | Desc

derive instance Generic Sort _

sortToString :: Sort -> String
sortToString = case _ of
  Asc -> "asc"
  Desc -> "desc"

sortFromString :: String -> Either String Sort
sortFromString = case _ of
  "asc" -> Right Asc
  "desc" -> Right Desc
  val -> Left $ "Not a sort: " <> val

sort :: RouteDuplex' String -> RouteDuplex' Sort
sort = as sortToString sortFromString

-- define the api routes
api :: RouteDuplex' Route
api = root $ G.sum
  { "Home": G.noArgs
  , "Profile": "profile" / string segment
  , "Account": "account" / string segment
  , "Search": "search" ? { q: string, sorting: optional <<< sort }
  }

-- example of a second api for meta information 
data Route2 = Health

derive instance Generic Route2 _

meta :: RouteDuplex' Route2
meta = root $ G.sum { "Health": "health" / G.noArgs }

-- optionally define a custom notFoundHandler
notFoundHandler :: Request Unit -> ResponseM
notFoundHandler = const $ ok "Nothing to see here"

-- combine routes using `<+>`, combine routes using `orElse`
main :: ServerM
main = serve { port: 8080, notFoundHandler } { route: api <+> meta, router: apiRouter `orElse` metaRouter }
  where

  apiRouter { route: Home } = ok "hello world!"
  apiRouter { route: (Profile profile) } = ok $ "hello " <> profile <> "!"
  apiRouter { route: (Account account) } = found' redirect ""
    where
    -- to create a redirect just print the data constructor
    redirect = headers [ Tuple "Location" $ print api $ Profile account ]

  apiRouter { route: (Search { q, sorting }) } = ok $ "searching for query " <> q <> " " <> case sorting of
    Just Asc -> "ascending"
    Just Desc -> "descending"
    Nothing -> "defaulting to ascending"

  metaRouter { route: Health } = ok """{"status":"ok"}"""

Startup options

HTTPurple 🪁 greatly simplifies the startup options and functions. The serve, serve', serveSecure and serveSecure' have been merged into a single function serve that accepts listen options as the first parameter and uses sane defaults if you don't provide any.

The easiest way to start a server is to provide just the route and a router:

main :: ServerM
main =
  serve {} { route, router }

This will spin up the http server with sane defaults.

HTTPurple 🪁 up and running on http://0.0.0.0:8080

But you can overwrite any of the optional properties like this

main :: ServerM
main =
  serve {
    hostname: "localhost"
  , port: 9000
  , certFile: "./Certificate.cer"
  , keyFile: "./Key.key"
  , notFoundHandler
  , onStarted: log "Server started 🚀"
  , closingHandler: NoClosingHandler
  } { route, router }
  where
  notFoundHandler :: Request Unit -> ResponseM
  notFoundHandler = const $ ok "Nothing to see here"

Other improvmenets

  • Default closing handler - A default closing handler is provided so you can just stop your server using ctrl+x without having to worry about anything. You can deactivate it by setting closingHandler: NoClosingHandler in the listen options.

Hot module reloading

With HTTPurple 🪁 you can easily set up a hot module reloading workflow:

Create an index.js with the content:

import * as Main from './output/Main/index.js'
Main.main()

Add to package.json:

  ...
  "scripts": {
      "hot": "spago build -w & nodemon \"node index.js\""
    },
  "type": "module",
  ...

Spin up:

npm run hot

Develop:

HTTPurple 🪁 up and running on http://0.0.0.0:8080
[nodemon] restarting due to changes...
[nodemon] restarting due to changes...
[nodemon] starting `node "node index.js" index.js`
HTTPurple 🪁 up and running on http://0.0.0.0:8080
[nodemon] restarting due to changes...
[nodemon] restarting due to changes...
[nodemon] starting `node "node index.js" index.js`
HTTPurple 🪁 up and running on http://0.0.0.0:8080

License

This is a fork of HTTPure, which is licensed under MIT. See the original license. This work is similarly licensed under MIT.