Delivering Declarative Data to DoltHub with GraphQL

REFERENCEWEB
4 min read

DoltHub is GitHub for data. As you might imagine, the data-fetching needs on the front end of such an application are intense. In the previous article in this series, we saw how working directly with our gRPC API was making our front-end team rather unhappy, and how we remedied our sadness using GraphQL. In this article we discuss how our GraphQL server is implemented and how we connect to it from our client Next.js app.

Server Implementation

As we discussed in the previous article, a GraphQL server typically sits in front of another API or set of APIs and provides a friendly query language to access that data. This allows you to declare data dependencies in your application very easily and precisely, which are fulfilled in intelligent ways by the GraphQL client and server.

NestJS

Our GraphQL server is powered by NestJS, a backend Node framework written in TypeScript with an architecture heavily inspired by the Angular front-end framework. We use Nest's built-in GraphQL support, which ties together a few different GraphQL technologies to create a convenient and powerful development experience.

NestJS supports a code-first approach, in which our GraphQL schema is automatically generated from special decorators in our TypeScript, instead of us writing it by hand in GraphQL's schema language (the schema-first approach).

@ObjectType()
export class User {
  @Field()
  _id: string;
  @Field()
  username: string;
  @Field({ nullable: true })
  displayName?: string;
  @Field((_type) => [email.EmailAddress])
  emailAddressesList: email.EmailAddress[];
}

An abbreviated version of our user model. Note the ObjectType and Field decorators...lots of them.

This is admittedly a little cumbersome as-is, thanks to the limitations of TypeScript's metadata reflection. Using a plugin for the NestJS CLI, these decorators can be added at build time, and we can simply write:

@ObjectType()
export class User {
  _id: string;
  username: string;
  displayName?: string;
  emailAddressesList: email.EmailAddress[];
}

Oooh, not bad at all!

NestJS will correctly infer the GraphQL types from the TypeScript type information at build time.

Resolvers

A GraphQL server accepts queries (read operations) and mutations (write operations), and it resolves those into data. The collections of functions that you write for this purpose are called your resolvers. Generally, each major kind of thing in the system, like repositories, users, tables, etc, has its own resolver and at least one model associated with it.

@Resolver(_of => Repo)
export class RepoResolver implements ResolverInterface<Repo> {
  constructor(
    private readonly api: APIProvider,
  ) {}

  @Query(_returns => Repo)
  async repo(@Context() context, @Args() repo: GetRepoArgs): Promise<Repo> {
    const api = this.api.client(context);
    const req = newGetRepositoryRequest(repo);
    const repo = await api.getRepository(req)
    return fromAPIModel(repo);
  }

An abbreviated version of our GraphQL resolver for repositories. Note the Resolver and Query decorators.

As mentioned in the last article, one of the main reasons that we wrote the GraphQL server was to put a layer in front of our gRPC API. Now the resolvers are where we call that API, instead of in the front-end code. Furthermore, the GraphQL server is a great place to get the data from the gRPC API into a suitable format for display. Code like this previously ended up all over our front-end application:

export function fromAPIModel(u: api.User): User {
  const { name: _id } = u.toObject();
  const username = UserUtils.username(u);
  return {
    _id,
    username,
    displayName: u.getDisplayName() || undefined,
    emailAddressesList: u
      .getEmailAddressesList()
      .map((a) => email.fromAPIModel(a)),
  };
}

Translations like these now have a home in the GraphQL server instead of in our front-end application.

Once we’ve written our resolvers, we connect them to the main NestJS application module and we’re ready to go. Under the hood, NestJS uses TypeGraphQL to compile the schema when we run on the server. It uses Apollo Server to serve our GraphQL endpoint.

Next.js with Apollo

For our front-end application we use Next.js. We needed to pick a GraphQL client to use with it. There are many to choose from, but since Apollo is very well supported, popular, and is what we use on the server, we just went with that.

Next.js provides an official example of integrating Apollo, but we actually simplified ours a little bit because, at least for now, we aren’t actually finding that we need to do any server-side calls at all. In general, we are trying to avoid server-side rendering now; static generation is all the rage these days, don’t you know?

export function withApollo<P extends object = {}>() {
  return (PageComponent: NextPage<P>) => {
    const WithApollo = ({
      apolloClient,
      apolloState,
      ...pageProps
    }: ApolloContext) => {
      const client = apolloClient || initApolloClient(apolloState, undefined);
      return (
        <ApolloProvider client={client}>
          <PageComponent {...(pageProps as P)} />
        </ApolloProvider>
      );
    };
    // Set the correct displayName in development
    if (process.env.NODE_ENV !== "production") {
      setDisplayName(WithApollo, PageComponent);
    }
    return WithApollo;
  };
}

Our withApollo higher-order component.

Now, in the subtree of any page component we wrap this way, we can use Apollo's React hooks to easily query data:

import { useQuery } from "@apollo/react-hooks";
import { gql } from "@apollo/client";
import UserCard from "./UserCard";

export default function() {
  const { data, error, loading } = useQuery(gql`
    query CurrentUser {
      currentUser {
        _id
        username
        displayName
      }
    }
  `);
  if (loading) return <ReactLoader loaded={false} />;
  if (error) return <ErrorMsg err={error} />;
  if (!data) return <ErrorMsg errTxt="No data returned by the server">;
  return <UserCard user={data.currentUser} />;
}

Simple example of querying data with Apollo's React hooks. Caching, loading, errors, and things like refetching are all handled nicely for us.

Already, this is a much cleaner and more robust way of working with our API data, but we're just getting started as far as developer experience. In the next article in this series, we'll see how we use tools in the GraphQL ecosystem to make working with it even more convenient and ergonomic. In the meantime, go play around with DoltHub and notice how responsive it is. Much of that is due to the caching and loading capabilities of Apollo.

Please let us know if there's anything you'd like to see us cover on this blog, or if you have any other suggestions. We're always interested in your feedback!

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.