we began our series on production Haskell techniques by learning about . We created a schema that contained a single type that we could store in a Postgresql database. We examined a couple functions allowing us to make SQL queries about these users. Last week Persistent User This week, we’ll see how we can expose this database to the outside world using an API. We’ll construct our API using the library. Servant involves some advanced type level constructs, so there’s a lot to wrap your head around. There are definitely simpler approaches to HTTP servers than what Servant uses. But I’ve found that the power Servant gives us is well worth the effort. Servant This article will give a brief overview on Servant. But if you want a more in-depth introduction, you should check out my last spring! That talk was more exhaustive about the different combinators you can use in your APIs. It also showed authentication techniques, client functions and documentation. You can also check out the for that presentation! talk from Bayhac slides and code Also, take a look at the on the Github repo for this project to see all the code for this article! servant branch Defining our API The first step in writing an API for our user database is to decide what the different endpoints are. We can decide this independent of what language or library we’ll use. For this article, our API will have two different endpoints. The first will be a POST request to . This request will contain a “user” definition in its body, and the result will be that we’ll create a user in our database. Here’s a sample of what this might look like: /users POST /users{ userName : “John Doe”, userEmail : “john@doe.com”, userAge : 29, userOccupation: “Teacher”} It will then return a response containing the database key of the user we created. This will allow any clients to fetch the user again. The second endpoint will use the ID to fetch a user by their database identifier. It will be a GET request to . So for instance, the last request might have returned us something like . We could then do the following: /users/:userid 16 GET /users/16 And our response would look like the request body from above. An API as a Type So we’ve got our very simple API. How do we actually define this in Haskell, and more specifically with Servant? Well, Servant does something pretty unique (as far I’ve researched). In Servant we define our API by using a type. Our type will include sub-types for each of the endpoints of our API. We combine the different endpoints by using the operator. I'll sometimes refer to this as “E-plus”, for “endpoint-plus”. This is a type operator, like some of the operators we saw with . Here’s the blueprint of our API: (:<|>) dependent types and tensor flow type UsersAPI = fetchEndpoint :<|> createEndpoint Now let’s define what we mean by and . Endpoints combine different combinators that describe different information about the endpoint. We link combinators together with the operator, which I call “C-plus” (combinator plus). Here’s what our final API looks like. We’ll go through what each combinator means in the next section: fetchEndpoint createEndpoint (:>) type UsersAPI = “users” :> Capture “userid” Int64 :> Get ‘[JSON] User :<|> “users” :> ReqBody ‘[JSON] User :> Post ‘[JSON] Int64 Different Combinators Both of these endpoints have three different combinators. Let’s start by examining the fetch endpoint. It starts off with a string combinator. This is a path component, allowing us to specify what url extension the caller should use to hit to endpoint. We can use this combinator multiple times to have a more complicated path for the endpoint. If we instead wanted this endpoint to be at then we’d change it to: /api/users/:userid “api” :> “users” :> Capture “userid” Int64 :> Get ‘[JSON] User The second combinator ( ) allows us to get a value out of the URL itself. We give this value a name and then we supply a type parameter. We won't have to do any path parsing or manipulation ourselves. Servant will handle the tricky business of parsing the URL and passing us an . If you want to use your own custom class as a piece of HTTP data, that's not too difficult. You’ll just have to write an instance of the class. All the basic types like already have instances. Capture Int64 FromHttpApiData Int64 The final combinator itself contains three important pieces of information for this endpoint. First, it tells us that this is in fact a request. Second, it gives us the list of content-types that are allowable in the response. This is a type level list of content formats. Each type in this list must have different classes for serialization and deserialization of our data. We could have used a more complicated list like . But for the rest of this article, we’ll just use . This means we'll use the and typeclasses for serialization. GET ’[JSON, PlainText, OctetStream] JSON ToJSON FromJSON The last piece of this combinator is the type our endpoint returns. So a successful request will give the caller back a response that contains a in JSON format. Notice this isn’t a . If the ID is not in our database, we’ll return a 401 error to indicate failure, rather than returning . User Maybe User Nothing Our second endpoint has many similarities. It uses the same string path component. Then its final combinator is the same except that it indicates it is a request instead of a request. The second combinator then tells us what we can expect the request body to look like. In this case, the request body should contain a JSON representation of a . It requires a list of acceptable content types, and then the type we want, like the and combinators. POST GET User Get Post That completes the “definition” of our API. We’ll need to add and instances of our type in order for this to function. You can take a look at those on , and check out for more details on creating those instances! ToJSON FromJSON User Github this article Writing Handlers Now that we’ve defined the type of our API, we need to write handler functions for each endpoint. This is where Servant’s awesomeness kicks in. We can map each endpoint up to a function that has a particular type based on the combinators in the endpoint. So, first let’s remember our endpoint for fetching a user: “users” :> Capture “userid” Int64 :> Get ‘[JSON] User The string path component doesn’t add any arguments to our function. The component will result in a parameter of type that we’ll need in our function. Then the return type of our function should be . This almost completely defines the type signature of our handler. We'll note though that it needs to be in the monad. So here’s what it’ll look like: Capture Int64 User Handler fetchUsersHandler :: Int64 -> Handler User... Servant can also look at the type for our create endpoint: “users” :> ReqBody ‘[JSON] User :> Post ‘[JSON] Int64 The parameter for a parameter is just the type argument. So it will resolve the endpoint into the handler type: ReqBody createUserHandler :: User -> Handler Int64... Now, we’ll need to be able to access our Postgres database through both these handlers. So they’ll each get an extra parameter referring to the . We’ll pass that from our code so that by the time Servant is resolving the types, the parameter is accounted for: ConnectionString fetchUsersHandler :: ConnectionString -> Int64 -> Handler UsercreateUserHandler :: ConnectionString -> User -> Handler Int64 Before we go any further, we should discuss the monad. This is a wrapper around the monad . In other words, each of these requests might fail. To make it fail, we can throw errors of type . Then of course we can also call functions, because these are network operations. Handler ExceptT ServantErr IO ServantErr IO Before we implement these functions, let’s first write a couple simple helpers. These will use the function from last week’s article to run database actions: runAction fetchUserPG :: ConnectionString -> Int64 -> IO (Maybe User)fetchUserPG connString uid = runAction connString (get (toSqlKey uid)) createUserPG :: ConnectionString -> User -> IO Int64createUserPG connString user = fromSqlKey <$> runAction connString (insert user) For completeness (and use later in testing), we’ll also add a simple function. We need the clause for type inference: delete where deleteUserPG :: ConnectionString -> Int64 -> IO ()deleteUserPG connString uid = runAction connString (delete userKey) where userKey :: Key User userKey = toSqlKey uid Now from our Servant handlers, we’ll call these two functions. This will completely cover the case of the create endpoint. But we’ll need a little bit more logic for the fetch endpoint. Since our functions are in the monad, we have to lift them up to . IO Handler fetchUsersHandler :: ConnectionString -> Int64 -> Handler UserfetchUserHandler connString uid = do maybeUser <- liftIO $ fetchUserPG connString uid ... createUserHandler :: ConnectionString -> User -> Handler Int64createuserHandler connString user = liftIO $ createUserPG connString user To complete our fetch handler, we need to account for a non-existent user. Instead of making the type of the whole endpoint a , we’ll throw a in this case. We can use one of the built-in Servant error functions, which correspond to normal error codes. Then we can update the body. In this case, we’ll throw a 401 error. Here’s how we do that: Maybe ServantErr fetchUsersHandler :: ConnectionString -> Int64 -> Handler UserfetchUserHandler connString uid = do maybeUser <- lift $ fetchUserPG connString uid case maybeUser of Just user -> return user Nothing -> Handler $ (throwE $ err401 { errBody = “Could not find user with ID: “ ++ (show uid)}) createUserHandler :: ConnectionString -> User -> Handler Int64createuserHandler connString user = lift $ createUserPG connString user And that’s it! We’re done with our handler functions! Combining it All into a Server Our next step is to create an object of type over our API. This is actually remarkably simple. When we defined the original type, we combined the endpoints with the operator. To make our , we do the same thing but with the handler functions: Server (:<|>) Server usersServer :: ConnectionString -> Server UsersAPIusersServer pgInfo = (fetchUsersHandler pgInfo) :<|> (createUserHandler pgInfo) And Servant does all the work of ensuring that the type of each endpoint matches up with the type of the handler! It’s pretty awesome. Suppose we changed the type of our so that it took a instead of an . We’d get a compile error: fetchUsersHandler Key User Int64 fetchUsersHandler :: ConnectionString -> Int64 -> Handler User… -- Compile Error!• Couldn't match type ‘Key User’ with ‘Int’ Expected type: Server UsersAPI Actual type: (Key User -> Handler User) :<|> (User -> Handler Int64) There’s now a mismatch between our API definition and our handler definition. So Servant knows to throw an error! The one issue is that the error messages can be rather difficult to interpret sometimes. This is especially the case when your API becomes very large! The “Actual type” section of the above error will become massive! So always be careful when changing your endpoints! is your friend! Frequent compilation Building the Application The final piece of the puzzle is to actually build an object out of our server. The first step of this process is to create a for our API. Remember that our API is a type, and not a term. But a allows us to represent this type at the term level. The concept is a little complicated, but the code is not! Application Proxy Proxy import Data.Proxy … usersAPI :: Proxy UsersAPIusersAPI = Proxy :: Proxy UsersAPI Now we can make our runnable like so (assuming we have a Postgres connection): Application serve usersAPI (usersServer connString) We’ll run this server from port 8000 by using the function, again from . (See for a full list of imports). We’ll fetch our connection string, and then we’re good to go! run Network.Wai Github runServer :: IO ()runServer = do pgInfo <- fetchPostgresConnection run 8000 (serve usersAPI (usersServer pgInfo)) Conclusion The Servant library offers some truly awesome possibilities. We’re able to define a web API at the type level. We can then define handler functions using the parameters the endpoints expect. Servant handles all the work of marshalling back and forth between the HTTP request and the native Haskell types. It also ensures a match between the endpoints and the handler function types! If you want to see even more of the possibilities that Servant offers, you should watch my . It goes through some more advanced concepts like authentication and client side functions. You can get the slides and all the code examples for that talk . talk from Bayhac here If you’ve never tried Haskell before, there’s no time like the present to start! Download our for some tools to help start your Haskell journey! Getting Started Checklist