[ANN] ResGraph - build implementation-first GraphQL servers in ReScript

ResGraph let’s you build “implementation-first”, Relay compliant GraphQL servers using ReScript.

Implementation first means that instead of defining your schema in GraphQL SDL or via a schema builder, the schema is derived from the code you write. The example from the readme should illustrate implementation first well. This ReScript code:

@gql.type
type query

/** A timestamp. */
@gql.scalar
type timestamp = float

/** A thing with a name. */
@gql.interface
type hasName = {@gql.field name: string}

@gql.type
type user = {
  ...hasName,
  @gql.field /** When this user was created. */ createdAt: timestamp,
  @gql.field @deprecated("Use 'name' instead") fullName: string,
}

/** Format for text. */
@gql.enum
type textFormat = Uppercase | Lowercase | Capitalized

/** The user's initials, e.g 'Alice Smith' becomes 'AS'. */
@gql.field
let initials = (user: user, ~format=Uppercase) => {
  let initials = getInitials(user.name)

  switch format {
  | Uppercase | Capitalized => initials->String.toUpperCase
  | Lowercase => initials->String.toLowerCase
  }
}

/** The current time on the server. */
@gql.field
let currentTime = (_: query): timestamp => {
  Date.now()
}

/** The currently logged in user, if any. */
@gql.field
let loggedInUser = async (_: query, ~ctx: ResGraphContext.context): option<user> => {
  switch ctx.currentUserId {
  | None => None
  | Some(userId) => await ctx.dataLoaders.userById.load(~userId)
  }
}

…generates this GraphQL schema:

type Query {
  """
  The current time on the server.
  """
  currentTime: Timestamp!

  """
  The currently logged in user, if any.
  """
  loggedInUser: User
}

"""
A timestamp.
"""
scalar Timestamp

type User implements HasName {
  """
  When this user was created.
  """
  createdAt: Timestamp!
  fullName: String! @deprecated(reason: "Use 'name' instead")

  """
  The user's initials, e.g 'Alice Smith' becomes 'AS'.
  """
  initials(format: TextFormat): String!
  name: String!
}

"""
A thing with a name.
"""
interface HasName {
  name: String!
}

"""
Format for text.
"""
enum TextFormat {
  Uppercase
  Lowercase
  Capitalized
}

A more hands-on example of using ResGraph with dataloaders, context etc is available in the project template repository.

ResGraph is fully driven by codegen, and works by deriving a GraphQL schema from the type information the compiler gives us. This has two main benefits - a) as much work as possible is done statically rather than in runtime, and b) your compilation speed will not suffer.

Using the type information from the compiler also means you can make full use of inference and all other ReScript goodies. It also leverages new ReScript features like record type spreads, unboxed variants and more from the upcoming ReScript v11.

It generates an optimized graphql-js schema, meaning you have access to the broad GraphQL JavaScript ecosystem. ResGraph also ships with a bunch of (opinionated) helpers to make building solid GraphQL servers following best practices simple and hopefully enjoyable. It even has a dedicated LSP and VSCode extension providing ResGraph’s error messages directly in the editor, autocomplete, hovers, snippets, and more.

ResGraph can be considered beta at this point, but slowly approaching a stable first version.

I’m very interested in hearing your feedback if you try it! I’m personally excited about this given that I’ve been building (Relay compliant) GraphQL servers for a long time, but not really found a solid and smooth workflow. Turns out if you view GraphQL as a slightly dumb subset of ReScript’s type system, things can feel a lot better.

14 Likes