avatar deldreth
Walter Melon

GraphQL primer part two

Previously, I covered the basics of GraphQL schema definition including types, queries, and mutations. In part two I'll be diving into creating a GraphQL service and server with Prisma, MySQL, and Apollo Server.

Previously: GraphQL primer part one

I'll also be including two links to repos that can be used to follow along. The first has the base docker compose setup needed to get started. The other is the end result for the server. This article does assume some basic experience/knowledge with Docker. I've selected Prisma for this portion largely due to my familiarity. There are, however, other great projects such as Hasura that offer similar features.

Prisma also creates a useful electron app, graphql-playground, that can be used to interact with the service here. Other's like GraphiQL can be used too.

Preperation

Docker can be obtained here. Follow the instructions to get it setup. After you've gotten Docker setup and authenticated with the Docker Hub you'll be able to install the images necessary for this portion.

I'm going to be placing the definition for the data layer inside the same project as the server. The basic directory structure in this case will be:

- database/
  - datamodel.graphql
  - docker-compose.yml
  - prisma.yml
- src/
  - index.js
  - schema.graphql
- .graphqlconfig.yml

The docker-compose.yml file will define two images one running our Prisma service and the other running our MySQL server. You can see it broken down in more detail within the Prisma docs and the one for this service is vanilla.

version: '3'
services:
  prisma:
    image: prismagraphql/prisma:1.14
    restart: always
    ports:
      - '4466:4466'
    environment:
      PRISMA_CONFIG: |
        port: 4466
        databases:
          default:
            connector: mysql
            host: mysql
            port: 3306
            user: root
            password: prisma
            migrations: true
  mysql:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: prisma
    volumes:
      - mysql:/var/lib/mysql
volumes: mysql:

We also need a file to configure Prisma's external interface. In the same database directory we have a prisma.yml.

endpoint: http://localhost:4466
datamodel: datamodel.graphql

That's it for the setup! Once you've got it set you can use the following command to pull the images and start your backend services.

docker-compose -f database/docker-compose.yml up -d

Now to actually writing our service.

Writing a GraphQL service on top of MySQL

For quick reference here's the final schema that we created in part one.

type Location {
  id: ID!
  name: String!
  cats: [Cat!]!
}

type Cat {
  id: ID!
  name: String!
  age: Int
  weight: Float
  breed: String
  location: Location
}

input LocationInput {
  name: String!
}

input CatInput {
  name: String!
  age: Int
  weight: Float
  breed: String
}

type Query {
  location(id: ID!): Location!
  cat(id: ID!): Cat!
  getLocations(): [Location]!
}

type Mutation {
  addLocation(input: LocationInput): Location!
  addCat(locationId: ID!, input: CatInput): Cat!
}

schema {
  query: Query
  mutation: Mutation
}

For our data access layer we need to break this down into the datamodel.graphql file that Prisma is going to process to build our access schema. In this case we don't actually need our input, query and mutation, or schema types. Prisma also introduces some specific directives to our schema that it uses to extend the data definitions. The one we'll be using in our case is @unique. There are others like @default and @relation.

Prisma datamodel

In this case we only need our two main types: Location and Cat. Notice that I've added @unique to the id field. This will instruct Prisma that the field should be an auto increment key.

# database/datamodel.graphql
type Location {
  id: ID! @unique
  name: String!
  cats: [Cat!]!
}

type Cat {
  id: ID! @unique
  name: String!
  age: Int
  weight: Float
  breed: String
  location: Location
}

That's it! Relatively simple for the case of this project. If you're following along you can place the contents of that snippet in database/datamodel.graphql. Once there you can deploy your datamodel to the dockerized Prisma service with the following command.

npx prisma deploy

If you've gotten green lights across the board then that means you have a working data layer mapped through Prisma to your MySQL server. If you've installed a GraphQL client you should be able to inspect the schema at http://localhost:4466. Take note about how this schema differs from our intended one. Prisma makes a lot of useful assumptions about how we want to faciliate interacting with our data. It provides a number of ORM like types for querying data. We will use this schema to write the server.

Generated schema

The graphql cli provides a number of useful commands to work with schemas and services. In this case we need to generate the schema for the GraphQL server. It can even generate template projects for servers and clients. It's quite useful for getting started but we specifically want to focus on the get-schema command. It's going to allow us to generate a new schema file for our server. Before we do that, however, we need to create a .graphqlconfig.yml file.

.graphqlconfig.yml

In the root directory of our server we'll create a .graphqlconfig.yml' (or json, if you prefer) and provide it with two project directivesappanddatabase`. You could potentially have any number of projects represented in a single file. This file is extremely useful in team situations. Many GraphQL application can use this project file to manage connections to your services. Here we're just describing the location of our schemas (generated and database) as well as the endpoint on which the server will eventually be running.

#.graphqlconfig.yml
projects:
  app:
    schemaPath: src/schema.graphql
    extensions:
      endpoints:
        default: http://localhost:4000
  database:
    schemaPath: src/generated/prisma.graphql
    extensions:
      prisma: database/prisma.yml

Here is graphql-playground after opening the project directory with the above .graphqlconfig. Two projects app and database, and the default configurations for them.

GraphQL Playground

If you're interested in learning more about the graphql-config protocol checkout the specification.

Now that our .graphqlconfig is ready you may noticed the src/generated/prisma.graphl and that there's no such file in our project yet.

If you run the following command:

❯ npx graphql get-schema --project database

You should see:

project database - Schema file was created: src/generated/prisma.graphql

Inspecting the generated/prisma.graphql file shows a rather lengthy and complex schema that we did not actually define. This generated schema is what Prisma exposes from our data layer. If you're using graphql-playground with the two services defined above running you can see this schema upon inspection.

GraphQL Playground

It's not important to go over all that's created by the generated schema, but it is a good idea to familiarize yourself with it. A number of the types defined within it will be used to create our server.

Writing a GraphQL server with Apollo Server

To keep things simple for our server we will only be adding two new files. An index.js file and the rest of our schema. Up until now we've only been supplying the Prisma service with the data portions of our schema: Locations and Cats. Now we need to take the input types, queries, and mutations, and let Apollo Server know about them. Because the schema is interpreted at runtime and the Locations and Cats of our schema exist outside of the application side we need another package graphql-import to allows us to import types from one schema to another.

The rest of the schema

In the src directory create a schema.graphql file with the following contents. Here we're bringing the application side of our schema together with the data size. As mentioned before graphql-import is going to parse the schema and interpret the generated types at runtime.

# import Location from './generated/prisma.graphql'
# import Cat from './generated/prisma.graphql'

input LocationInput {
  name: String!
}

input CatInput {
  name: String!
  age: Int
  weight: Float
  breed: String
}

type Query {
  location(id: ID!): Location!
  cat(id: ID!): Cat!
  getLocations: [Location]!
}

type Mutation {
  addLocation(input: LocationInput): Location!
  addCat(locationId: ID!, input: CatInput): Cat!
}

schema {
  query: Query
  mutation: Mutation
}

Resolvers

Now that we're about to write the server for our data we begin talking about resolvers. At a most basic level a resolver is just a function that resolves a field. Often enough a function that queries a database. They can exist at both the client and server side (depending on your setup) and be used to resolve any type including custom scalars, and most commonly queries and mutations.

Resolvers are functions that resolve data to a field.

If you've been following along from the base repository linked at the beginning of this article you've probably noticed a bare bones index.js in src. It looks something like this:

// src/index.js
const { ApolloServer } = require('apollo-server');
const { importSchema } = require('graphql-import');
const { Prisma } = require('prisma-binding');
const path = require('path');

const resolvers = {};

const server = new ApolloServer({
  typeDefs: importSchema(path.resolve('src/schema.graphql')),
  resolvers,
  context: (req) => ({
    ...req,
    prisma: new Prisma({
      typeDefs: 'src/generated/prisma.graphql',
      endpoint: 'http://localhost:4466',
    }),
  }),
});

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`🐈  🐈  🐈  ready at ${url}`);
});

There's no magic here. We're creating an Apollo Server at port 4000. I've imported importSchema from graql-import to handle the custom imports within our application schema. And I'm also providing custom context to each request in the form of a Prisma instance. I've specifically left the resolvers object literal empty in order to define them now. Apollo Server will use the keys in the resolvers map to bind resolution of fields. For example, if we look at our application schema we only have two types we need to worry about creating resolvers for at this time: Query and Mutation. The key to graphql type binding is 1:1 so if we update the resolvers map to something like:

const resolvers = {
  Query: {},
};

We are actually informing Apollo Server of the resolution of the Query type in our schema. I'm sure you can guess what's next. Defining the functions that resolve to the queries. All resolvers for Apollo Server have the following signature:

fieldName(obj, args, context, info) { result }

A quick breakdown of the parameters for a resolver... obj - The result returned from a resolver of a parent field. In the case of nested fields this parameter provides further resolution for children. args - Object containing key value pairs of arguments passed to the query. context - Shared execution context from Apollo Server. In the case of our server this is where Prisma will be accesible. info - Contains information about the execution state of the query.

Query resolvers

In the application schema we have three queries and our stubbed out resolvers can look something like:

const resolvers = {
  Query: {
    location: (obj, args, context, info) => {},
    cat: (obj, args, context, info) => {},
    getLocations: (obj, args, context, info) => {},
  },
};

Of course, these won't function as is. We need to actually specify the resolution. In this case we're querying Prisma based on the generated schema. We do so through the context object. You can inspect the schema from Prisma to determine which fields need to be queried. For our queries we will just be using the location, cat, and locations types. You can also see what arguments the generated schema provides.

GraphQL Playground

For our location query resolver we can use the where argument from Prisma's schema to limit the results.

const resolvers = {
  Query: {
    location: (obj, args, context, info) => {
      return context.prisma.query.location(
        {
          where: { id: args.id },
        },
        info
      );
    },
    cat: (obj, args, context, info) => {},
    getLocations: (obj, args, context, info) => {},
  },
};

We now have one working resolver for our location query. It will take the id argument, query Prisma, which in turn queries MySQL, and resolves our location. Continuing, our query resolvers will look something like:

const resolvers = {
  Query: {
    location: (obj, args, context, info) => {
      return context.prisma.query.location(
        {
          where: { id: args.id },
        },
        info
      );
    },
    cat: (obj, args, context, info) => {
      return context.prisma.query.cat(
        {
          where: { id: args.id },
        },
        info
      );
    },
    getLocations: (obj, args, context, info) => {
      return context.prisma.query.locations({}, info);
    },
  },
};

Mutation resolvers

I've shown what makes up a resolver for a query. Mutations are really not so different. They're still using Prisma to add or modify data.

const resolvers = {
  Query: {...},
  Mutation: {
    addCat: (obj, args, context, info) => {
      return context.prisma.mutation.createCat(
        {
          data: {
            name: args.input.name,
            age: args.input.age,
            weight: args.input.weight,
            breed: args.input.breed,
            location: {
              connect: {
                id: args.locationId
              }
            }
          }
        },
        info
      );
    },
    addLocation: (obj, args, context, info) => {
      return context.prisma.mutation.createLocation(
        {
          data: {
            name: args.input.name
          }
        },
        info
      );
    }
  }
};

Here I'm mapping the input type arguments to the data object for the createCat and createLocation mutations. The result of the operation will be the resolved object (based on our schema).

Wrapping up

With the resolvers defined the server can function as expected. The example repository for this includes nodemon so you could run npx nodemon src/index.js to start the server. If you were to use graphql-playground to open the project directory (with the .graphqlconfig.yml file) you would see a two viable graphql schemas at the endpoints provided. One, the server, at 4000 and Prisma at 4466.

In summation the server should look something like this:

const { ApolloServer } = require('apollo-server');
const { importSchema } = require('graphql-import');
const { Prisma } = require('prisma-binding');
const path = require('path');

const resolvers = {
  Query: {
    location: (obj, args, context, info) => {
      return context.prisma.query.location(
        {
          where: { id: args.id },
        },
        info
      );
    },
    cat: (obj, args, context, info) => {
      return context.prisma.query.cat(
        {
          where: { id: args.id },
        },
        info
      );
    },
    getLocations: (obj, args, context, info) => {
      return context.prisma.query.locations({}, info);
    },
  },
  Mutation: {
    addCat: (obj, args, context, info) => {
      return context.prisma.mutation.createCat(
        {
          data: {
            name: args.input.name,
            age: args.input.age,
            weight: args.input.weight,
            breed: args.input.breed,
            location: {
              connect: {
                id: args.locationId,
              },
            },
          },
        },
        info
      );
    },
    addLocation: (obj, args, context, info) => {
      return context.prisma.mutation.createLocation(
        {
          data: {
            name: args.input.name,
          },
        },
        info
      );
    },
  },
};

const typeDefs = importSchema(path.resolve('src/schema.graphql'));

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: (req) => ({
    ...req,
    prisma: new Prisma({
      typeDefs: 'src/generated/prisma.graphql',
      endpoint: 'http://localhost:4466',
    }),
  }),
});

server.listen({ port: 4000 }).then(({ url }) => {
  console.log(`🐈  🐈  🐈  ready at ${url}`);
});

This is a relatively straight forward server and schema. In some situations future resolvers could be defined for nested types but in general the Prisma generated schema fits most situations. You may also notice that all of queries to Prisma include the info arugment. This allows Prisma to determine the context of the query and resolve types based on its own schema.

By now I've been mentioning graphql-playground at length. Largely because it's quite useful for someone working in a team with a project that has multiple application schemas. There are others like graphiql that work just as well. Each have standalone desktop applications (if you prefer that).