Update changelog and docs
This commit is contained in:
parent
d6df08a726
commit
306edb6bd7
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
## Unpublished
|
## Unpublished
|
||||||
|
|
||||||
- Add json request parsing simplifications
|
## V1.2.0
|
||||||
|
|
||||||
|
- Add json request parsing
|
||||||
- Add request validation
|
- Add request validation
|
||||||
|
|
||||||
## v1.1.0
|
## v1.1.0
|
||||||
|
155
docs/Basics.md
155
docs/Basics.md
@ -1,16 +1,22 @@
|
|||||||
# HTTPure Basics
|
# HTTPurple Basics
|
||||||
|
|
||||||
This guide is a brief overview of the basics of creating a HTTPure server.
|
This guide is a brief overview of the basics of creating a HTTPurple server.
|
||||||
|
|
||||||
|
## TOC
|
||||||
|
|
||||||
|
1. [Creating a Server](#creating-a-server)
|
||||||
|
1. [Hot module reloading](#hot-module-reloading)
|
||||||
|
1. [Further server settings](#further-server-settings)
|
||||||
|
|
||||||
## Creating a Server
|
## Creating a Server
|
||||||
|
|
||||||
To create a server, use `HTTPure.serve` (no SSL) or `HTTPure.serveSecure` (SSL).
|
To create a server, use `HTTPurple.serve`.
|
||||||
Both of these functions take a port number, a router function, and an `Effect`
|
Both of these functions take a port number, a router function, and an `Effect`
|
||||||
that will run once the server has booted. The signature of the router function
|
that will run once the server has booted. The signature of the router function
|
||||||
is:
|
is:
|
||||||
|
|
||||||
```purescript
|
```purescript
|
||||||
HTTPure.Request -> HTTPure.ResponseM
|
HTTPurple.Request route -> HTTPurple.ResponseM
|
||||||
```
|
```
|
||||||
|
|
||||||
For more details on routing, see the [Routing guide](./Routing.md). For more
|
For more details on routing, see the [Routing guide](./Routing.md). For more
|
||||||
@ -18,63 +24,113 @@ details on responses, see the [Responses guide](./Responses.md). The router can
|
|||||||
be composed with middleware; for more details, see the [Middleware
|
be composed with middleware; for more details, see the [Middleware
|
||||||
guide](./Middleware.md).
|
guide](./Middleware.md).
|
||||||
|
|
||||||
## Non-SSL
|
You can create an HTTPurple server using `HTTPurple.serve`:
|
||||||
|
|
||||||
You can create an HTTPure server without SSL using `HTTPure.serve`:
|
|
||||||
|
|
||||||
```purescript
|
```purescript
|
||||||
main :: HTTPure.ServerM
|
import Prelude hiding ((/))
|
||||||
main = HTTPure.serve 8080 router $ log "Server up"
|
|
||||||
```
|
|
||||||
|
|
||||||
Most of the [examples](./Examples), besides [the SSL Example](./Examples/SSL),
|
import HTTPurple
|
||||||
use this method to create the server.
|
|
||||||
|
|
||||||
You can also create a server using a custom
|
data Route = Hello String
|
||||||
[`HTTP.ListenOptions`](http://bit.ly/2G42rLd) value:
|
derive instance Generic Route _
|
||||||
|
|
||||||
```purescript
|
route :: RouteDuplex' Route
|
||||||
main :: HTTPure.ServerM
|
route = mkRoute
|
||||||
main = HTTPure.serve' customOptions router $ log "Server up"
|
{ "Hello": "hello" / segment
|
||||||
```
|
}
|
||||||
|
|
||||||
## SSL
|
main :: ServerM
|
||||||
|
|
||||||
You can create an SSL-enabled HTTPure server using `HTTPure.serveSecure`, which
|
|
||||||
has the same signature as `HTTPure.serve` except that it additionally takes a
|
|
||||||
path to a cert file and a path to a key file after the port number:
|
|
||||||
|
|
||||||
```purescript
|
|
||||||
main :: HTTPure.ServerM
|
|
||||||
main =
|
main =
|
||||||
HTTPure.serveSecure 8080 "./Certificate.cer" "./Key.key" router $
|
serve { port: 8080 } { route, router }
|
||||||
log "Server up"
|
where
|
||||||
|
router { route: Hello name } = ok $ "hello " <> name
|
||||||
|
```
|
||||||
|
|
||||||
|
`HTTPurple.serve` takes as arguments two records:
|
||||||
|
1. Server configuration - A record containing all additional settings that you want to pass. See [further server settings](#further-server-settings) for a list of all settings.
|
||||||
|
1. A record containing your route and a router for these routes. See the [routing guide](./Routing.md) for more information.
|
||||||
|
|
||||||
|
|
||||||
|
## Hot module reloading
|
||||||
|
|
||||||
|
With HTTPurple 🪁 you can easily set up a hot module reloading workflow:
|
||||||
|
|
||||||
|
Create an `index.js` with the content:
|
||||||
|
```javascript
|
||||||
|
import * as Main from './output/Main/index.js'
|
||||||
|
Main.main()
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `package.json`:
|
||||||
|
```json
|
||||||
|
...
|
||||||
|
"scripts": {
|
||||||
|
"hot": "spago build -w & nodemon \"node index.js\""
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Spin up:
|
||||||
|
```bash
|
||||||
|
npm run hot
|
||||||
|
```
|
||||||
|
Develop:
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Further server settings
|
||||||
|
|
||||||
|
HTTPurple 🪁 defines a series of settings that you can override.
|
||||||
|
|
||||||
|
Here is an example of the full list of server settings:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
hostname: "localhost"
|
||||||
|
, port: 9000
|
||||||
|
, certFile: "./Certificate.cer"
|
||||||
|
, keyFile: "./Key.key"
|
||||||
|
, notFoundHandler: custom404Handler
|
||||||
|
, onStarted: log "Server started 🚀"
|
||||||
|
, closingHandler: NoClosingHandler
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL
|
||||||
|
|
||||||
|
**Note**: SSL is usually something that you want to handle at the infrastructure level and not within the application's http server. The SSL support is mainly here because HTTPure had it, but I might remove it in the near future if it hinders development.
|
||||||
|
|
||||||
|
You can create an SSL-enabled HTTPurple server using `HTTPurple.serve` by passing a certFile, a keyFile and an optionally a different port:
|
||||||
|
```purescript
|
||||||
|
main :: HTTPurple.ServerM
|
||||||
|
main =
|
||||||
|
HTTPurple.serve { port: 443, certFile : "./Certificate.cer", keyFile: "./Key.key" } { route, router }
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
You can look at [the SSL Example](./Examples/SSL/Main.purs), which uses this
|
You can look at [the SSL Example](./Examples/SSL/Main.purs), which uses this
|
||||||
method to create the server.
|
method to create the server.
|
||||||
|
|
||||||
You can also create a server using a
|
|
||||||
[`HTTP.ListenOptions`](http://bit.ly/2G42rLd) and a
|
|
||||||
[`HTTPS.SSLOptions`](http://bit.ly/2G3Aljr):
|
|
||||||
|
|
||||||
|
### Closing handler
|
||||||
|
|
||||||
|
HTTPurple 🪁 comes with a default closing handler, so `Ctrl+x` just stops the server.
|
||||||
|
you can switch off this behaviour by passing
|
||||||
```purescript
|
```purescript
|
||||||
main :: HTTPure.ServerM
|
{ closingHandler: NoClosingHandler }
|
||||||
main =
|
|
||||||
HTTPure.serveSecure' customSSLOptions customOptions router $
|
|
||||||
log "Server up"
|
|
||||||
```
|
```
|
||||||
|
to `serve` and define your own closing handler:
|
||||||
|
|
||||||
## Shutdown hook
|
|
||||||
|
|
||||||
To gracefully shut down a server you can add a shutdown hook. For this you will need to add the following dependencies:
|
|
||||||
|
|
||||||
```
|
|
||||||
posix-types
|
|
||||||
node-process
|
|
||||||
```
|
|
||||||
|
|
||||||
Then take the closing handler returned by `serve` and create a `SIGINT` and `SIGTERM` hook:
|
|
||||||
|
|
||||||
```purescript
|
```purescript
|
||||||
import Prelude
|
import Prelude
|
||||||
@ -82,14 +138,11 @@ import Prelude
|
|||||||
import Data.Posix.Signal (Signal(SIGINT, SIGTERM))
|
import Data.Posix.Signal (Signal(SIGINT, SIGTERM))
|
||||||
import Effect (Effect)
|
import Effect (Effect)
|
||||||
import Effect.Console (log)
|
import Effect.Console (log)
|
||||||
import HTTPure (serve, ok)
|
import HTTPurple (serve, ok)
|
||||||
import Node.Process (onSignal)
|
import Node.Process (onSignal)
|
||||||
|
|
||||||
main :: Effect Unit
|
main :: Effect Unit
|
||||||
main = do
|
main = do
|
||||||
closingHandler <- serve 8080 (const $ ok "hello world!") do
|
closingHandler <- serve 8080 { route, router }
|
||||||
log $ "Server now up on port 8080"
|
-- do something with closingHandler
|
||||||
|
|
||||||
onSignal SIGINT $ closingHandler $ log "Received SIGINT, stopping service now."
|
|
||||||
onSignal SIGTERM $ closingHandler $ log "Received SIGTERM, stopping service now."
|
|
||||||
```
|
```
|
||||||
|
@ -7,7 +7,6 @@ If you have used HTTPure before, you'll probably want to go through the followin
|
|||||||
* [startup options](#startup-options)
|
* [startup options](#startup-options)
|
||||||
* [request parsing and validation](#request-parsing-and-validation)
|
* [request parsing and validation](#request-parsing-and-validation)
|
||||||
* [other improvements](#other-improvmenets)
|
* [other improvements](#other-improvmenets)
|
||||||
* [hot module reloading](#hot-module-reloading)
|
|
||||||
|
|
||||||
## Routing-duplex
|
## Routing-duplex
|
||||||
|
|
||||||
@ -118,61 +117,8 @@ main =
|
|||||||
|
|
||||||
## Request parsing and validation
|
## Request parsing and validation
|
||||||
|
|
||||||
HTTPurple 🪁 makes request parsing and validation super simple. My typical http service scenario looks like this:
|
HTTPurple 🪁 has some helpers to make json parsing and validation very simple. See the [requests guide](./Requests.md) for more information.
|
||||||
1. Parse the request json and return a bad request if the request body doesn't contain the valid json format
|
|
||||||
2. Validate the json input semanticall and transform it into some kind of internal model. Return bad request (with some error code) in case it is invalid.
|
|
||||||
3. Do something with the request
|
|
||||||
4. Return the output as a json
|
|
||||||
|
|
||||||
HTTPurple 🪁 uses continuations to make this standard scenario straight-forward (see example below).
|
|
||||||
|
|
||||||
Furthermore, HTTPurple 🪁 doesn't mandate a json parsing library. So you can use [`argonaut`](https://github.com/purescript-contrib/purescript-argonaut) using the [`argonaut-driver`](https://github.com/sigma-andex/purescript-httpurple-argonaut), use [`yoga-json`](https://github.com/rowtype-yoga/purescript-yoga-json) using the [`yoga-json-driver`](https://github.com/sigma-andex/purescript-httpurple-yoga-json) or write your own json driver.
|
|
||||||
|
|
||||||
Here is an example how that looks like:
|
|
||||||
```purescript
|
|
||||||
apiRouter { route: Home, method: Post, body } = usingCont do
|
|
||||||
req@{ name } :: HelloWorldRequest <- fromJson Argonaut.jsonDecoder body
|
|
||||||
ok $ "hello " <> name <> "!"
|
|
||||||
```
|
|
||||||
In case `fromJson` succeeds, the next step will be executed, otherwise a 400 bad request is returned.
|
|
||||||
|
|
||||||
## Other improvmenets
|
## 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.
|
* 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:
|
|
||||||
```javascript
|
|
||||||
import * as Main from './output/Main/index.js'
|
|
||||||
Main.main()
|
|
||||||
```
|
|
||||||
|
|
||||||
Add to `package.json`:
|
|
||||||
```json
|
|
||||||
...
|
|
||||||
"scripts": {
|
|
||||||
"hot": "spago build -w & nodemon \"node index.js\""
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Spin up:
|
|
||||||
```bash
|
|
||||||
npm run hot
|
|
||||||
```
|
|
||||||
Develop:
|
|
||||||
```bash
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
96
docs/Requests.md
Normal file
96
docs/Requests.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Requests
|
||||||
|
|
||||||
|
This section describes how to work with requests bodys. For information about routing see the [routing docs](Routing.md).
|
||||||
|
|
||||||
|
## TOC
|
||||||
|
1. [tl;dr](#tldr)
|
||||||
|
1. [introduction](#introduction)
|
||||||
|
1. [json request parsing](#1-json-request-parsing)
|
||||||
|
1. [data validation](#2-data-validation)
|
||||||
|
1. [business logic](#3-your-business-logic)
|
||||||
|
1. [output json](#4-output-json)
|
||||||
|
|
||||||
|
## tl;dr
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
router { route: Home, method: Post, body } = usingCont do
|
||||||
|
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
|
||||||
|
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
|
||||||
|
output :: MyOutput <- lift $ doSomethingAndReturnAff input
|
||||||
|
ok' jsonHeaders $ toJson Argonaut.jsonEncoder output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
A typical micro-service scenario looks like this:
|
||||||
|
1. You get a json request, so the first thing you need to do is to parse your json request format and return a bad request if it doesn't match your format.
|
||||||
|
2. Then you do some kind of data/business validation of the input. Is the field `email` really an email? So you want to do this semantic validation and return a bad request (with some error code) in case it is invalid.
|
||||||
|
3. Now since you have the validated input data, you can do your actual logic and produce some output data.
|
||||||
|
4. Finally you want to dump this output data as json
|
||||||
|
|
||||||
|
HTTPurple 🪁 provides a very simple way of handling this scenario based on continuations. Let's go through each step:
|
||||||
|
|
||||||
|
## 1. Json request parsing
|
||||||
|
|
||||||
|
HTTPurple 🪁 provides a `fromJson` method that makes json parsing of your body super simple. `fromJson` is library agnostic, so you can use [`argonaut`](https://github.com/purescript-contrib/purescript-argonaut) using the [`argonaut-driver`](https://github.com/sigma-andex/purescript-httpurple-argonaut), use [`yoga-json`](https://github.com/rowtype-yoga/purescript-yoga-json) using the [`yoga-json-driver`](https://github.com/sigma-andex/purescript-httpurple-yoga-json) or write your own json driver.
|
||||||
|
If you don't know which one to use, I would recommend to go with the `argonaut` driver.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# for argonaut install the argonaut driver:
|
||||||
|
spago install httpurple-argonaut
|
||||||
|
|
||||||
|
# for yoga-json install the yoga-json driver:
|
||||||
|
spago install httpurple-yoga-json
|
||||||
|
```
|
||||||
|
|
||||||
|
`fromJson` returns a continuation, so just start your handler implementation with `usingCont` and then use `fromJson` with `Argonaut.jsonDecoder` or `Yoga.jsonDecoder`:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
router { route: Home, method: Post, body } = usingCont do
|
||||||
|
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
|
||||||
|
```
|
||||||
|
As you can see, really simple. This parses the body into your format and returns a bad request if it fails. If you need to customise the bad request, there is `fromJsonE` which allows you to pass a custom bad request handler.
|
||||||
|
|
||||||
|
## 2. Data validation
|
||||||
|
|
||||||
|
Next, we will want do validate our json data. Or put in other words, we want to transform the weakly typed json model into a strongly-typed internal data model.
|
||||||
|
|
||||||
|
`fromValidated` takes a validation function of the shape `input -> Either error validated`:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
router { route: Home, method: Post, body } = usingCont do
|
||||||
|
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
|
||||||
|
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need a custom bad request handler, you can use `fromValidatedE` which takes an bad request handler as second parameter.
|
||||||
|
|
||||||
|
## 3. Your business logic
|
||||||
|
|
||||||
|
Now that you got your validated input, you can pass it to your business logic.
|
||||||
|
There is one caveat though, we are currently in a continuation, so you will need to `lift` your `Aff` returning function into the continuation monad:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
router { route: Home, method: Post, body } = usingCont do
|
||||||
|
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
|
||||||
|
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
|
||||||
|
output :: MyOutput <- lift $ myAffReturningFunction input
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Output json
|
||||||
|
|
||||||
|
Finally you can return your json. Use the `toJson` function which takes the json driver and your output your data as json:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
router { route: Home, method: Post, body } = usingCont do
|
||||||
|
jsonRequest :: MyRequest <- fromJson Argonaut.jsonDecoder body
|
||||||
|
input :: MyValidatedInput <- fromValidated validateMyRequest jsonRequest
|
||||||
|
output :: MyOutput <- lift $ doSomethingAndReturnAff input
|
||||||
|
ok' jsonHeaders $ toJson Argonaut.jsonEncoder output
|
||||||
|
```
|
||||||
|
|
||||||
|
If your business logic output is strongly typed, I would write a transformer function to a weakly typed json model instead of writing custom json encoders:
|
||||||
|
|
||||||
|
```purescript
|
||||||
|
ok' jsonHeaders $ toJson Argonaut.jsonEncoder $ transformToApi output
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user