7.1 KiB
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:
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:
HTTPure.Request -> HTTPure.ResponseM
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.
For more details about the response monad, see the Responses guide.
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 ofHTTPure.Method
.path
- AnArray
ofString
path segments. A path segment is a nonempty string separated by a"/"
. Empty segments are stripped out when HTTPure creates theHTTPure.Request
record.query
- AnObject
ofString
values. Note that if you have any query parameters without values (for instance, a URL like/foo?bar
), then the value in theObject
for that query parameter will be the emptyString
(""
).headers
- AHTTPure.Headers
object. TheHTTPure.Headers
newtype wraps theObject String
type and provides some typeclass instances that make more sense when working with HTTP headers.body
- AString
containing the contents of the request body, or an emptyString
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 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:
Lookup (Array t) Int t
- In this instance,HTTPure.lookup
is the same asArray.index
. Because the path is represented as anArray
ofStrings
, this can be used to retrieve the nth path segment by doing something likerequest.path !! n
.Lookup (Object t) String t
- In this instance,HTTPure.lookup
is a flipped version ofObject.lookup
. Because the query is aObject String
, this instance can be used to retrieve the value of a query parameter by name, by doing something likerequest.query !! "someparam"
.Lookup Headers String String
- This is similar to the example in #2, except that it works with theHTTPure.Headers
newtype, and the key is case-insensitive (sorequest.headers !! "X-Test" == request.headers !! "x-test"
).
There are three infix operators defined on the HTTPure.Lookup
typeclass that
are extremely useful for routing:
!!
- This is an alias toHTTPure.lookup
itself, and returns aMaybe
containing some type.!@
- This is the same asHTTPure.lookup
, but it returns the actual value instead of aMaybe
containing the value. It only operates on instances ofHTTPure.Lookup
where the return type is aMonoid
, and returnsmempty
ifHTTPure.lookup
returnsNothing
. It's especially useful when routing based on specific values in query parameters, path segments, or header fields.!?
- This returnstrue
if the key on the right hand side is in the data set on the left hand side. In other words, ifHTTPure.lookup
matches something, this istrue
, otherwise, this isfalse
.
Matching HTTP Methods
You can use normal pattern matching to route based on the HTTP method:
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 module. To see an example server that routes based on the HTTP method, see the Post example.
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:
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:
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:
router { path } = HTTPure.ok $ "Path segment 0: " <> path !@ 0
To see an example server that works with path segments, see the Path Segments example.
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:
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:
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:
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.
Working With Request Headers
Headers are again very similar to working with path segments or query parameters:
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.