Add guides (#85)
This commit is contained in:
parent
133b98c9c6
commit
c79e9e4346
44
docs/Basics.md
Normal file
44
docs/Basics.md
Normal file
@ -0,0 +1,44 @@
|
||||
# HTTPure Basics
|
||||
|
||||
This guide is a brief overview of the basics of creating a HTTPure server.
|
||||
|
||||
## Creating a Server
|
||||
|
||||
To create a server, use `HTTPure.serve` (no SSL) or `HTTPure.serve'` (SSL). Both
|
||||
of these functions take a port number, a router function, and an `Eff` that will
|
||||
run once the server has booted. The signature of the router function is:
|
||||
|
||||
```purescript
|
||||
forall e. HTTPure.Request -> HTTPure.ResponseM e
|
||||
```
|
||||
|
||||
For more details on routing, see the [Routing guide](./Routing.md). For more
|
||||
details on responses, see the [Responses guide](./Responses.md). The router can
|
||||
be composed with middleware; for more details, see the [Middleware
|
||||
guide](./Middleware.md).
|
||||
|
||||
## Non-SSL
|
||||
|
||||
You can create an HTTPure server without SSL using `HTTPure.serve`:
|
||||
|
||||
```purescript
|
||||
main = HTTPure.serve 8080 router $ Console.log "Server up"
|
||||
```
|
||||
|
||||
Most of the [examples](./Examples), besides [the SSL Example](./Examples/SSL),
|
||||
use this method to create the server.
|
||||
|
||||
## SSL
|
||||
|
||||
You can create an SSL-enabled HTTPure server using `HTTPure.serve'`, 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.serve 8080 "./Certificate.cer" "./Key.key" router $
|
||||
Console.log "Server up"
|
||||
```
|
||||
|
||||
You can look at [the SSL Example](./Examples/SSL/Main.purs), which uses this
|
||||
method to create the server.
|
110
docs/Middleware.md
Normal file
110
docs/Middleware.md
Normal file
@ -0,0 +1,110 @@
|
||||
# Writing and Using Middleware in HTTPure
|
||||
|
||||
Since HTTPure routers are just pure functions, you can write a middleware by
|
||||
simply creating a function that takes a router and an `HTTPure.Request`, and
|
||||
returns an `HTTPure.ResponseM`. You can then simply use function composition to
|
||||
combine middlewares, and pass your router to your composed middleware to
|
||||
generate the decorated router!
|
||||
|
||||
See [the Middleware example](./Examples/Middleware/Main.purs) to see how you can
|
||||
build, compose, and consume different types of middleware.
|
||||
|
||||
## Writing Middleware
|
||||
|
||||
A middleware is a function with the signature:
|
||||
|
||||
```purescript
|
||||
forall e. (HTTPure.Request -> HTTPure.ResponseM e) ->
|
||||
HTTPure.Request ->
|
||||
HTTPure.ResponseM e
|
||||
```
|
||||
|
||||
Note that the first argument is just the signature for a router function. So
|
||||
essentially, your middleware should take a router and return a new router.
|
||||
That's it! You can do pretty much anything with middlewares. Here are a few
|
||||
examples of common middleware patterns:
|
||||
|
||||
You can write a middleware that wraps all future work in some behavior, like
|
||||
logging or timing:
|
||||
|
||||
```purescript
|
||||
myMiddleware router request = do
|
||||
doSomethingBefore
|
||||
response <- router request
|
||||
doSomethingAfter
|
||||
pure response
|
||||
```
|
||||
|
||||
Or perhaps a middleware that injects something into the response:
|
||||
|
||||
```purescript
|
||||
myMiddleware router request = do
|
||||
response <- router request
|
||||
HTTPure.response' response.status response.headers $
|
||||
response.body <> "\n\nGenerated using my super duper middleware!"
|
||||
```
|
||||
|
||||
You could even write a middleware that handles routing for some specific cases:
|
||||
|
||||
```purescript
|
||||
myMiddleware _ { path: [ "somepath" ] } = HTTPure.ok "Handled by my middleware!"
|
||||
myMiddleware router request = router request
|
||||
```
|
||||
|
||||
Or even a middleware that conditionally includes another middleware:
|
||||
|
||||
```purescript
|
||||
myMiddleware router = if something then someOtherMiddleware router else router
|
||||
```
|
||||
|
||||
Just make sure your middlewares follow the correct signature, and users will be
|
||||
able to compose them at will!
|
||||
|
||||
Note that because there is nothing fancy happening here, you could always write
|
||||
higher order functions that don't follow this signature, if it makes sense. For
|
||||
instance, you could write a function that takes two routers, and selects which
|
||||
one to use based on some criteria. There is nothing wrong with this, but you
|
||||
should try to use the middleware signature mentioned above as much as possible
|
||||
as it will make your middleware easier to consume and compose.
|
||||
|
||||
## Consuming Middleware
|
||||
|
||||
Consuming middleware easy: simply compose all the middleware you want, and then
|
||||
pass your router to the composed middleware. For instance:
|
||||
|
||||
```purescript
|
||||
main = HTTPure.serve port composedRouter $ Console.log "Server is up!"
|
||||
where
|
||||
composedRouter = middlewareA <<< middlewareB <<< middlewareC $ router
|
||||
```
|
||||
|
||||
Be aware of the ordering of the middleware that you compose--since we used
|
||||
`<<<`, the middlewares will compose right-to-left. But because middlewares
|
||||
choose when to apply the router to the request, this is a bit like wrapping the
|
||||
router in each successive middleware from right to left. So when the router
|
||||
executes on a request, those middlewares will actually _execute_
|
||||
left-to-right--or from the outermost wrapper inwards.
|
||||
|
||||
In other words, say you have the following HTTPure server:
|
||||
|
||||
```purescript
|
||||
middleware letter router request = do
|
||||
EffClass.liftEff $ Console.log $ "Starting Middleware " <> letter
|
||||
response <- router request
|
||||
EffClass.liftEff $ Console.log $ "Ending Middleware " <> letter
|
||||
pure response
|
||||
|
||||
main = HTTPure.serve port composedRouter $ Console.log "Server is up!"
|
||||
where
|
||||
composedRouter = middleware "A" <<< middleware "B" $ router
|
||||
```
|
||||
|
||||
When this HTTPure server receives a request, the logs will include:
|
||||
|
||||
```
|
||||
Starting Middleware A
|
||||
Starting Middleware B
|
||||
...
|
||||
Ending Middleware B
|
||||
Ending Middleware A
|
||||
```
|
89
docs/Responses.md
Normal file
89
docs/Responses.md
Normal file
@ -0,0 +1,89 @@
|
||||
# Creating HTTPure Responses
|
||||
|
||||
## The Response Monad
|
||||
|
||||
The `HTTPure.ResponseM` monad is the return type of the router function. It is
|
||||
an `Aff` type that contains an `HTTPure.Response`. Because it is an `Aff`, it
|
||||
transparent to add asynchronous behavior when you need.
|
||||
|
||||
To see an example server taking advantage of asynchronous responses, see [the
|
||||
Async Response example](./Examples/AsyncResponse/Main.purs).
|
||||
|
||||
## Response Helpers
|
||||
|
||||
HTTPure defines a number of helpers for creating response monads for all
|
||||
different HTTP response status codes. Some of these helpers take a body, for
|
||||
instance, `HTTPure.ok` and `HTTPure.partialContent`, and some do not, for
|
||||
instance `HTTPure.created` or `HTTPure.noContent`. There are prime functions
|
||||
corresponding to each helper--for instance, `HTTPure.ok'` and
|
||||
`HTTPure.created'`. The prime versions are the same as the base versions except
|
||||
they also return response headers--see the [Setting Response
|
||||
Headers](#setting-response-headers) section below for more details.
|
||||
|
||||
For a full list of helpers, see the [Response](../src/HTTPure/Response.purs)
|
||||
module.
|
||||
|
||||
In some cases, the defined helpers don't cover your needs--for instance, if
|
||||
HTTPure doesn't have a helper defined for some HTTP response code you care about
|
||||
(and you don't want to [contribute it](../Contributing.md)--hint hint, you
|
||||
should contribute it!), or if you need to specify a body where normally one
|
||||
wouldn't be sent. For these cases, you can use `HTTPure.response`, which takes a
|
||||
status code and a body. If you need to specify headers, use `HTTPure.response'`.
|
||||
If you don't need to specify a body, you can use `HTTPure.emptyResponse` or
|
||||
`HTTPure.emptyResponse'`.
|
||||
|
||||
## Raw Responses
|
||||
|
||||
The `HTTPure.ResponseM` monad contains a `HTTPure.Response` value, which is a
|
||||
`Record` type containing the following fields:
|
||||
|
||||
- `status` - An `Int` HTTP response status code.
|
||||
- `headers` - A `HTTPure.Headers` containing the HTTP response headers.
|
||||
- `body` - A `String` containing the HTTP response body.
|
||||
|
||||
You can manually construct a response if you want to:
|
||||
|
||||
```purescript
|
||||
router _ = pure $ { status: 200, headers: HTTPure.headers [], body: "foo" }
|
||||
```
|
||||
|
||||
This can be useful in some circumstances, but in the vast majority of cases it's
|
||||
recommended that you use the response helpers described above -- they are more
|
||||
explicit and allow you to avoid using magic numbers in your code for HTTP status
|
||||
codes.
|
||||
|
||||
## Setting Response Headers
|
||||
|
||||
If you need to return response headers, you can do so using the prime versions
|
||||
of the response helpers. These functions take an `HTTPure.Headers` object. You
|
||||
can construct an `HTTPure.Headers` in a few ways:
|
||||
|
||||
- `HTTPure.empty` - Construct an empty `HTTPure.Headers`
|
||||
- `HTTPure.header` - Given a string with a header name and a string with a
|
||||
value, construct a singleton `HTTPure.Headers`. For instance:
|
||||
|
||||
```purescript
|
||||
headers = HTTPure.header "X-My-Header" "value"
|
||||
```
|
||||
|
||||
- `HTTPure.headers` - Construct a `HTTPure.Headers` from an `Array` of `Tuples`
|
||||
of two `Strings`, where the first `String` is the header name and the second
|
||||
`String` is the header value. For instance:
|
||||
|
||||
```purescript
|
||||
headers = HTTPure.headers
|
||||
[ Tuple "X-Header-A" "valueA"
|
||||
, Tuple "X-Header-B" "valueB"
|
||||
]
|
||||
```
|
||||
|
||||
Because `HTTPure.Headers` has an instance of `Semigroup`, you can also append
|
||||
`HTTPure.Headers` objects:
|
||||
|
||||
```purescript
|
||||
headers =
|
||||
HTTPure.header "X-Header-A" "valueA" <> HTTPure.header "X-Header-B" "valueB"
|
||||
```
|
||||
|
||||
To see an example server that sets response headers, see [the Headers
|
||||
example](./Examples/Headers/Main.purs).
|
179
docs/Routing.md
Normal file
179
docs/Routing.md
Normal file
@ -0,0 +1,179 @@
|
||||
# Routing in HTTPure
|
||||
|
||||
Routing in HTTPure is designed on the simple principle of allowing PureScript to
|
||||
do what PureScript does best. When you create an HTTPure server, you pass it a
|
||||
router function:
|
||||
|
||||
```purescript
|
||||
main = HTTPure.serve 8080 router $ Console.log "Server up"
|
||||
```
|
||||
|
||||
The router function is called for each inbound request to the HTTPure server.
|
||||
Its signature is:
|
||||
|
||||
```purescript
|
||||
forall e. HTTPure.Request -> HTTPure.ResponseM e
|
||||
```
|
||||
|
||||
So in HTTPure, routing is handled simply by the router being a pure function
|
||||
which is passed a value that contains all information about the current request,
|
||||
and which returns a response monad. There's no fancy path parsing and matching
|
||||
algorithm to learn, and everything is pure--you don't get anything or set
|
||||
anything, you simply define the return value given the input parameters, like
|
||||
any other pure function.
|
||||
|
||||
This is quite powerful, as all routing can be defined using the same PureScript
|
||||
pattern matching and guard syntax you use everywhere else. It allows you to
|
||||
break up your router to sub-routers easily, using whatever router grouping makes
|
||||
sense for your app. It also leads to some powerful patterns for defining and
|
||||
using middleware. For more details about defining and using middleware, see the
|
||||
[Middleware guide](./Middleware.md).
|
||||
|
||||
For more details about the response monad, see the [Responses
|
||||
guide](./Responses.md).
|
||||
|
||||
## The Request Record
|
||||
|
||||
The `HTTPure.Request` type is the input parameter for the router function. It is
|
||||
a `Record` type that contains the following fields:
|
||||
|
||||
- `method` - A member of `HTTPure.Method`.
|
||||
- `path` - An `Array` of `String` path segments. A path segment is a nonempty
|
||||
string separated by a `"/"`. Empty segments are stripped out when HTTPure
|
||||
creates the `HTTPure.Request` record.
|
||||
- `query` - A `StrMap` of `String` values. Note that if you have any query
|
||||
parameters without values (for instance, a URL like `/foo?bar`), then the
|
||||
value in the `StrMap` for that query parameter will be the empty `String`
|
||||
(`""`).
|
||||
- `headers` - A `HTTPure.Headers` object. The `HTTPure.Headers` newtype wraps
|
||||
the `StrMap String` type and provides some typeclass instances that make more
|
||||
sense when working with HTTP headers.
|
||||
- `body` - A `String` containing the contents of the request body, or an empty
|
||||
`String` if none was provided.
|
||||
|
||||
Following are some more details on working with specific fields, but remember,
|
||||
you can combine guards and pattern matching for any or all of these fields
|
||||
however it makes sense for your use case.
|
||||
|
||||
## The Lookup Typeclass
|
||||
|
||||
You will find that much of HTTPure routing takes advantage of implementations of
|
||||
the [HTTPure.Lookup](../src/HTTPure/Lookup.purs) typeclass. This typeclass
|
||||
defines the function `HTTPure.lookup` (or the infix version `!!`), along with a
|
||||
few auxiliary helpers, for looking up a field out of an object with some key.
|
||||
There are three instances defined in HTTPure:
|
||||
|
||||
1. `Lookup (Array t) Int t` - In this instance, `HTTPure.lookup` is the same as
|
||||
`Array.index`. Because the path is represented as an `Array` of `Strings`,
|
||||
this can be used to retrieve the nth path segment by doing something like
|
||||
`request.path !! n`.
|
||||
2. `Lookup (StrMap t) String t` - In this instance, `HTTPure.lookup` is a
|
||||
flipped version of `StrMap.lookup`. Because the query is a `StrMap String`,
|
||||
this instance can be used to retrieve the value of a query parameter by name,
|
||||
by doing something like `request.query !! "someparam"`.
|
||||
3. `Lookup Headers String String` - This is similar to the example in #2, except
|
||||
that it works with the `HTTPure.Headers` newtype, and the key is
|
||||
case-insensitive (so `request.headers !! "X-Test" == request.headers !!
|
||||
"x-test"`).
|
||||
|
||||
There are three infix operators defined on the `HTTPure.Lookup` typeclass that
|
||||
are extremely useful for routing:
|
||||
|
||||
1. `!!` - This is an alias to `HTTPure.lookup` itself, and returns a `Maybe`
|
||||
containing some type.
|
||||
2. `!@` - This is the same as `HTTPure.lookup`, but it returns the actual value
|
||||
instead of a `Maybe` containing the value. It only operates on instances of
|
||||
`HTTPure.Lookup` where the return type is a `Monoid`, and returns `mempty` if
|
||||
`HTTPure.lookup` returns `Nothing`. It's especially useful when routing based
|
||||
on specific values in query parameters, path segments, or header fields.
|
||||
3. `!?` - This returns `true` if the key on the right hand side is in the data
|
||||
set on the left hand side. In other words, if `HTTPure.lookup` matches
|
||||
something, this is `true`, otherwise, this is `false`.
|
||||
|
||||
## Matching HTTP Methods
|
||||
|
||||
You can use normal pattern matching to route based on the HTTP method:
|
||||
|
||||
```purescript
|
||||
router { method: HTTPure.Post } = HTTPure.ok "received a post"
|
||||
router { method: HTTPure.Get } = HTTPure.ok "received a get"
|
||||
router { method } = HTTPure.ok $ "received a " <> show method
|
||||
```
|
||||
|
||||
To see the list of methods that HTTPure understands, see the
|
||||
[Method](../src/HTTPure/Method.purs) module. To see an example server that
|
||||
routes based on the HTTP method, see [the Post
|
||||
example](./Examples/Post/Main.purs).
|
||||
|
||||
## Working With Path Segments
|
||||
|
||||
Generally, there are two use cases for working with path segments: routing on
|
||||
them, and using them as variables. When routing on path segments, you can route
|
||||
on exact path matches:
|
||||
|
||||
```purescript
|
||||
router { path: [ "exact" ] } = HTTPure.ok "matched /exact"
|
||||
```
|
||||
|
||||
You can also route on partial path matches. It's cleanest to use PureScript
|
||||
guards for this. For instance:
|
||||
|
||||
```purescript
|
||||
router { path }
|
||||
| path !@ 0 == "foo" = HTTPure.ok "matched something starting with /foo"
|
||||
| path !@ 1 == "bar" = HTTPure.ok "matched something starting with /*/bar"
|
||||
```
|
||||
|
||||
When using a path segment as a variable, simply extract the path segment using
|
||||
the `HTTPure.Lookup` typeclass:
|
||||
|
||||
```purescript
|
||||
router { path } = HTTPure.ok $ "Path segment 0: " <> path !@ 0
|
||||
```
|
||||
|
||||
To see an example server that works with path segments, see [the Path Segments
|
||||
example](./Examples/PathSegments/Main.purs).
|
||||
|
||||
## Working With Query Parameters
|
||||
|
||||
Working with query parameters is very similar to working with path segments. You
|
||||
can route based on the _existence_ of a query parameter:
|
||||
|
||||
```purescript
|
||||
router { query }
|
||||
| query !? "foo" = HTTPure.ok "matched a request containing the 'foo' param"
|
||||
```
|
||||
|
||||
Or you can route based on the _value_ of a query parameter:
|
||||
|
||||
```purescript
|
||||
router { query }
|
||||
| query !@ "foo" == "bar" = HTTPure.ok "matched a request with 'foo=bar'"
|
||||
```
|
||||
|
||||
You can of course also use the value of a query parameter to calculate your
|
||||
response:
|
||||
|
||||
```purescript
|
||||
router { query } = HTTPure.ok $ "The value of 'foo' is " <> query !@ "foo"
|
||||
```
|
||||
|
||||
To see an example server that works with query parameters, see [the Query
|
||||
Parameters example](./Examples/QueryParameters/Main.purs).
|
||||
|
||||
## Working With Request Headers
|
||||
|
||||
Headers are again very similar to working with path segments or query
|
||||
parameters:
|
||||
|
||||
```purescript
|
||||
router { headers }
|
||||
| headers !? "X-Foo" = HTTPure.ok "There is an 'X-Foo' header"
|
||||
| headers !@ "X-Foo" == "bar" = HTTPure.ok "The header 'X-Foo' is 'bar'"
|
||||
| otherwise = HTTPure.ok $ "The value of 'X-Foo' is " <> headers !@ "x-foo"
|
||||
```
|
||||
|
||||
Note that using the `HTTPure.Lookup` typeclass on headers is case-insensitive.
|
||||
|
||||
To see an example server that works with headers, see [the Headers
|
||||
example](./Examples/Headers/Main.purs).
|
Loading…
Reference in New Issue
Block a user