# Routing in HTTPurple 🪁 ## Table of contents * [Routing introduction](#introduction) * [The request record](#the-request-record) * [Matching HTTP methods](#matching-http-methods) * [Matching paths and query parameters](#matching-paths-and-query-parameters) * [Adding further types](#adding-further-types) * [Composing routes](#composing-routes) * [Reverse routing](#reverse-routing) * [Catch-all route](#catch-all-route) * [Routing prefixes](#routing-prefixes) * [Working with request headers](#working-with-request-headers) ## Routing introduction HTTPurple 🪁 uses [`routing-duplex`](https://github.com/natefaubion/purescript-routing-duplex) for routing. You'll need two things: 1. A data type representing your routes 2. A mapping between the data constructors of your data type and the paths Here an example: ```purescript import HTTPurple -- We define a data type representing our route -- In this case, a single route `SayHello` that accepts one parameter data Route = SayHello String derive instance Generic Route _ -- a generic instance is needed -- The mapping between our data constructor `SayHello` and -- the route /hello/ route :: RouteDuplex' Route route = mkRoute { "SayHello": "hello" / string segment } -- We then start the http server passing the route and a handler function (router) main :: ServerM main = serve {} { route, router } where router { route: SayHello name } = ok $ "hello " <> name <> "!" ``` The router function is called for each inbound request to the HTTPure server. Its signature is: ```purescript forall route. HTTPurple.Request route -> HTTPurple.ResponseM ``` So in HTTPurple, 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 `HTTPurple.Request route` type is the input parameter for the router function. It is a `Record` type that contains the following fields: - `method` - A member of `HTTPurple.Method`. - `route` - A data type representing your route with paths and query parameters. - `headers` - A `HTTPurple.Headers` object. The `HTTPurple.Headers` newtype wraps the `Object 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. ## Matching paths and query parameters Let's have a look at a bit more complex routing scenario. Imagine we are developing the backend service for a simple web shop.We want two define three routes: - `/` which returns the data of the start page - `/categories//products/` which takes two path parameters category name and product name and returns a certain product - `/search?q=&sorting=` which takes two query parameters, a search string and an optional sorting argument ```purescript -- We define three data types representing the three routes data Route = Home | Products String String -- the product route with two path parameters | Search { q :: String, sorting :: Maybe String } -- the search route with two query parameters, whereby sorting is optional derive instance Generic Route _ -- Next we define the route (mapping) route :: RouteDuplex' Route route = mkRoute { "Home": noArgs -- the root route / , "Products": "categories" / string segment / "products" / string segment , "Search": "search" ? { q: string, sorting: optional <<< string } } -- Finally, we pass the route (mapping) to the server and also define a route handler main :: ServerM main = serve { port: 8080 } { route: route, router } where router { route: Home } = ok "home" router { route: Products category product } = do ok $ "category=" <> category <> ", product=" <> product router { route: Search { q, sorting } } = ok $ "searching=" <> q <> "," <> case sorting of Just "asc" -> "ascending" _ -> "descending" ``` As you can see, in the route handler you can directly pattern match your data type. Pattern matching of the route is exhaustive so that you will get an error if you miss a route. ### Adding further types We can add further type information to our route data type. Instead of treating the path arguments of our product route as `String`, we can define newtypes for the arguments: ```purescript newtype Category = Category String instance Newtype Category String newtype Product = Product String instance Newtype Product String ``` We'll change our data constructor to accept the two newtypes: ```purescript | Products Category Product ``` We can then define new segment types to match the different arguments: ```purescript category :: RouteDuplex' Category category = _Newtype R.segment product :: RouteDuplex' Product product = _Newtype segment ``` Now we can update our route mapping: ```purescript "Products": "categories" / category / "products" / product ``` We can furthe add a more expressive type for our sorting query parameter ```purescript data Sort = Asc | Desc derive instance Generic Sort _ ``` We can then define the conversion to and from string and define a new `RouteDuplex`: ```purescript 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 ``` We can then update our `Route` data type to use the new `Sort` data type ```purescript | Search { q :: String, sorting :: Maybe Sort } ``` and use the new `sort` function in our route mapping: ```purescript "Search": "search" ? { q: string, sorting: optional <<< sort } ``` As you can see, the `RoutingDuplex` approach is quite powerful and once you got a grip on it, it is also rather straight-forward to use. I recommend you to also check out the [`routing-duplex` documentation](https://github.com/natefaubion/purescript-routing-duplex) which contains many more examples. ### Composing routes Sometimes you might want to define two route data types to structure your routing logically. Composing routes is straight-forward with HTTPurple. E.g. you could have an `ApiRoute` representing your business api and a `MetaRoute` for technical routes, such as a health check: ```purescript data ApiRoute = Home derive instance Generic Route _ api :: RouteDuplex' ApiRoute api = mkRoute { "Home": G.noArgs } data MetaRoute = Health derive instance Generic MetaRoute _ meta :: RouteDuplex' MetaRoute meta = mkRoute { "Health": "health" / G.noArgs } ``` You can compose these two routes using the `<+>` operator, and compose the routing handlers with `orElse`: ```purescript main :: ServerM main = serve { port: 8080, notFoundHandler } { route: api <+> meta, router: apiRouter `orElse` metaRouter } where apiRouter { route: Home } = ok "hello world!" metaRouter { route: Health } = ok """{"status":"ok"}""" ``` ### Reverse routing Reverse routing, e.g. for redirects or HATEOAS, is straight-forward using the `print` function from `routing-duplex`: ```purescript data Route = Old | New route :: RouteDuplex' Route route = mkRoute { "Old": "old" / RG.noArgs , "New": "new" / RG.noArgs } main :: ServerM main = serve { port: 8080 } { route, router } where router { route: Old } = found' redirect "" where redirect = headers [ Tuple "Location" $ print route $ New account ] ``` ### Catch-all route You can easily create a catch all route by using the predefined `catchAll`. ```purescript import HTTPurple data Route = CatchAll (Array String) -- list of paths derive instance Generic Route _ route :: RouteDuplex' Route route = mkRoute { "CatchAll": catchAll } main :: ServerM main = serve {} { route, router } where router { route: CatchAll paths } = ok $ "hello 🗺!" ``` ### Routing prefixes You can create a route prefix such as `/v1/` using the `prefix` method. Here is an example: ```purescript import HTTPurple data Route = Home -- list of paths derive instance Generic Route _ route :: RouteDuplex' Route route = root $ prefix "v1" $ sum { "Home": noArgs } main :: ServerM main = serve {} { route, router } where router { route: Home } = ok $ "hello 🗺!" ``` Now all requests need to be prefixed with `/v1/`. **Note**: `prefix` takes exactly one path segment. If you need two prefixes, e.g. for `/api/v1/` you need to use two prefixes: `root $ prefix "api" $ prefix "v1" $ sum`. ## Matching HTTP Methods You can use normal pattern matching to route based on the HTTP method: ```purescript router { method: HTTPurple.Post } = HTTPurple.ok "received a post" router { method: HTTPurple.Get } = HTTPurple.ok "received a get" router { method } = HTTPurple.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 Request Headers Headers are again very similar to working with path segments or query parameters: ```purescript router { headers } | headers !? "X-Foo" = HTTPurple.ok "There is an 'X-Foo' header" | headers !@ "X-Foo" == "bar" = HTTPurple.ok "The header 'X-Foo' is 'bar'" | otherwise = HTTPurple.ok $ "The value of 'X-Foo' is " <> headers !@ "x-foo" ``` Note that using the `HTTPurple.Lookup` typeclass on headers is case-insensitive. To see an example server that works with headers, see [the Headers example](./Examples/Headers/Main.purs).