Welcome to Ceramic Hacker

Hi, I'm Alexander (Sasha) Skvortsov. I'm a computer science and math major at Penn State. I'll be using this blog to share some of my projects and thoughts, mainly focused around software engineering and pottery.
BlogOCaml WebDev

[3/x] Building GraphQL APIs with Dream

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.

Previous articleNext article

Comments (0)

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.

9 days later
Sasha changed the title to [3/x] Building GraphQL APIs with Dream .
8 days later
Write a Reply...