- Edited
Hi, and welcome back to my OCaml web development project! Last time, we built a simple website with login/logout functionality to demonstrate the basics of backend development with Dream and Caqti. However, since our end-goal is to build our frontend as a single-page application that interacts with our backend through an API, we'll need to add said API to our site. In this post, we'll replace our POST endpoints for login/logout with GraphQL queries and mutations.
GraphQL is a protocol and query language for web APIs. You can think of it as analogous to REST or JSON:API. If you're not familiar with it, this article or the official documentation is a good place to get some background.
We'll create our API using Dream's support for the wonderful ocaml-graphql-server. For this example, we'll want to implement 3 things:
- A query to get the current user.
- A mutation to log in.
- A mutation to log out.
We'll start by defining a GraphQL Object that represents a user:
open Graphql_lwt
let user =
Schema.(
obj "user" ~fields:(fun _ ->
[
field "email"
~args:Arg.[]
~typ:(non_null string)
~resolve:(fun _ (u : Models.User.User.t) -> u.email);
]))
There's no reason to expose hashed passwords, so the only field will be a user's email. Of course, in a real system we probably wouldn't expose emails either for privacy reasons, but we do so here for simplicity.
We'll also define helper functions that implement our mutations for logging in and logging out:
let login req email password =
let%lwt u = Dream.sql req (Models.User.UserRepository.get email) in
match u with
| Some user ->
if Auth.Hasher.verify user.password password then
let%lwt () = Dream.set_session_field req "user_id" email in
Lwt.return true
else Lwt.return false
| None -> Lwt.return false
let logout req =
let%lwt () = Dream.invalidate_session req in
Lwt.return true
Note that these are very similar to the login/logout handlers we defined last time, except that here, the email and password arguments are given directly, not extracted from the request via Dream.form
.
Now that we have our user object and mutation helper functions, we can implement the full GraphQL schema:
open Graphql_lwt
let schema =
Schema.(
schema
[
io_field "current_user"
~args:Arg.[]
~typ:user
~resolve:(fun info () ->
let uid = Dream.session_field info.ctx "user_id" in
match uid with
| Some uid ->
Lwt_result.ok
(Dream.sql info.ctx (Models.User.UserRepository.get uid))
Dream.get "/static/**" (Dream.static "assets");
| None -> Lwt_result.return None);
]
~mutations:
[
io_field "login" ~typ:(non_null bool)
~args:
Arg.
[
arg "email" ~typ:(non_null string);
arg "password" ~typ:(non_null string);
]
~resolve:(fun info () email password ->
Lwt_result.ok (Mutation.login info.ctx email password));
io_field "logout" ~typ:(non_null bool) ~args:[]
~resolve:(fun info () -> Lwt_result.ok (Mutation.logout info.ctx));
])
Note that here, we use io_field
instead of field
, since the implementation of these queries and mutations uses Lwt for database operations. Each field specifies its arguments, return type, and a resolve
function that fetches query results and executes mutations. resolve
has access to the current Dream request
instance through info.ctx
, where info
is the first argument to resolve
.
Finally, the last thing we need to do is plug this into our Dream routing map:
let routes : Dream.route list =
[
Dream.any "/graphql" (Dream.graphql Lwt.return Nmgraphql.Schema.schema);
Dream.get "/graphiql" (Dream.graphiql "/graphql");
Dream.get "/**" Views.Index.get;
]
Compared to the last post, we've dropped the login/logout handlers in favor of a /graphql
endpoint, which will handle all queries and mutations. We also add a /graphiql
path, which allows us to use the GraphiQL IDE to test and debug our GraphQL implementation.
And that's it! Now our site has a GraphQL API, to which we can easily add new queries, mutations, and objects by extending the schema above. In the final version of my project I ended up extending this schema quite a bit. You can explore it via GraphiQL at https://cmpsc431.ceramichacker.com/graphiql.
In the next post, I'll introduce the Bonsai UI framework, and how I set up my project to use it for my frontend.