Creating a GraphQL API while utilizing your REST API

Using an existing REST API we'll learn how to create a GraphQL API around our existing services instead of a complete rewrite of our API.

Published

January 3, 2021 @ 8:12 PM

Last Updated

January 3, 2021 @ 8:12 PM

    graphqlrestapijavascript

Table of Contents

Introduction

GraphQL is a fun service to consume on the client-side. With its documentation, playground, and state management options such as relay or apollo, developer experience has been a blast for myself and my co-workers. There is a common misconception that you can use either REST OR GraphQL, but not both. You can have both! When I was learning I thought it was all in GraphQL or all in REST. To learn more about the difference between these services visit Understanding GraphQL through REST. Many of us are in a situation where we have an existing REST API that we don't want to completely abandon to start to try out GraphQL.

By the end, you'll have a GraphQL API leveraging a REST API of your choice in JavaScript. For the purpose of quickness I'll be utilizing the Covid Act Now API, but feel free to sub out your own REST API and models. If you will be using the same API I would recommend obtaining your API key now. It's immediate so no wait time needed! Let's get started.

Awesome

Building your API

We'll be setting up JavaScript with a hello world script, converting that to a GraphQL hello world endpoint, adding a REST data source, and making a relationship to combine API calls.

Project Setup

Project setup has some optional steps such as using nodemon to reload our app for us, and dotenv to load environment variables. This would be where you would add in the tools you prefer to work with in JavaScript.

Create Project

mkdir covid-graphql

Initialize an npm Package

npm init -y

Create a Source Directory and an index.js File

touch src/index.js

Add a hello world Log to index.js

console.log("hello world")

Open package.json and Add dev and start Scripts

 "dev": "nodemon src/index.js --watch src/",
 "start": "node src/index.js"
  • dev utilizes nodemon to watch for any changes in the src directory
  • start using node (lighter than nodemon in production)

Run npm run dev in Terminal

Making sure we're up and running and don't need to restart our app for every change going forward.

 hello world

Take it easy on me if you thought this was pointless. It will help someone.

Everybody love everybody

Install Dependencies

npm install apollo-server apollo-datasource-rest dotenv

apollo-server

Used to spin up an express server.

apollo-datasource-rest

Used to take advantage of caching and other class-based inheritance around REST sources.

dotenv

Used to pass in environment variables.

nodemon

Used in development mode so you don't need to restart your app every time there is a file change.

Create a Hello World Schema

This schema file will be where we define what we can do with our GraphQL API.

touch src/schema.js

// schema.js
const { gql } = require("apollo-server")

const typeDefs = gql`
  type Query {
    hello: String
  }
`

module.exports = typeDefs

What we're saying in the typeDefs object is when we go to query in our graphQL API, we're only able to query hello and we expect a nullable String return type. In order to make that response not nullable we would just add an exclamation point to the end like so hello: String!.

Build Apollo Server

// src/index.js
require("dotenv").config()
const { ApolloServer } = require("apollo-server")
const typeDefs = require("./schema")

const server = new ApolloServer({ typeDefs })
const port = process.env.port || 9000

server.listen(port).then(() => {
  console.log(`server running 🚀 http://localhost:${port}`)
})

Inspect at http://localhost:9000 to see your schema defined. If you happen to try querying hello you will realize the response is null. We need to still define our resolver response.

It's all good

Create Resolver for hello

Resolvers are where the actual work gets done. The schema sets the limitations of the API in terms of what the clients can do. Actually doing that work is the job of the resolver. Similar to the query and mutation explanation in my previous posts, we will return Query and Mutation if needed. Under query, we now add in our hello resolver to return hello world.

touch src/resolvers.js

module.exports = {
  Query: {
    hello: () => {
      return "hello world"
    }
  }
}

Add Resolver to src/index.js apollo-server Declaration

const resolvers = require("./resolvers")

const server = new ApolloServer({ typeDefs, resolvers })

At this point, when you query hello you should get your expected response.

hello world query

Hooking Up a REST Source

Here we're going to build a REST Data source class that will be able to be utilized in our resolver to leverage some actual results.

Create REST Data Source

mkdir src/datasources
touch src/datasources/CovidActNowAPI.js

const { RESTDataSource } = require("apollo-datasource-rest")

class CovidActNowAPI extends RESTDataSource {
  constructor() {
    super()
    this.baseURL = "https://api.covidactnow.org/v2/"
  }
}

module.exports = CovidActNowAPI

All we really need to do here is extend RESTDataSource to get access to in-memory caching, HTTP helper functions, and pagination. To learn more visit apollo-datasource-rest.

Create .env File and Add-in API Key from Introduction

Hopefully, you've stowed away the key after obtaining your API key from the Covid Act Now API.

What is this amateur hour

touch .env

COVID_ACT_NOW=YOUR_KEY_HERE

Add Some Methods to Get States and Counties in CovidActNowAPI Class

Taking a look at the Covid Act Now API documentation we can see JSON response can be hit at our baseURL(https://api.covidactnow.org/v2/) plus states for all US states, state/OH for an individual US state such as Ohio, counties for all US counties, and county/39001 for an individual county by fips plus our API key as a parameter. There is also an optional .timeseries we can append to include an array of values for things like metrics and actual figures. For getting a list of states or counties I'm choosing to avoid this as it's a much larger payload, but I am enabling the time series for individual US states and counties. Using this we can create functions to interact with the REST service now.

async getAllStates() {
    const response = await this.get(`states.json?apiKey=${process.env.COVID_ACT_NOW}`);
    return response || [];
}

async getState(stateCode) {
    const response = await this.get(
    `state/${stateCode}.timeseries.json?apiKey=${process.env.COVID_ACT_NOW}`
    );
    return response;
}

async getAllCounties() {
    const response = await this.get(`counties.json?apiKey=${process.env.COVID_ACT_NOW}`);
    return response || [];
}

async getCounty(fips) {
    const response = await this.get(
    `county/${fips}.timeseries.json?apiKey=${process.env.COVID_ACT_NOW}`
    );
    return response;
}

Add Data Source to apollo-graphql Server

// index.js
const CovidActNowAPI = require("./datasources/CovidActNowAPI")

const server = new ApolloServer({
  dataSources: () => ({
    covidApi: new CovidActNowAPI()
  }),
  typeDefs,
  resolvers
})

Update Schema to Reflect States and Counties as Queries

This should be your most time-consuming part. Relative to the Covid Act Now API they have a convenient models section that will show you some of the subcategory types you'll need to recreate to tell consumers of this API what is possible. My process here for any API is to just look through the JSON response and make sure all the data I want available is covered. Every nested object needs a type in your schema to be recognized. If you don't need it, don't make a type for it and it's ignored 😊 You can also reference apollo docs - build a schema to look at any syntax here such as the non-nullable(!), input parameters on a query or mutation (county(fips: String!): County), or array response([County]).

const typeDefs = gql`
  type Query {
    counties: [County!]!
    county(fips: String!): County
    states: [State!]!
    state(stateCode: String!): State
  }

  type County {
    fips: String!
    country: String!
    state: String!
    county: String!
    population: Int!
    metrics: Metric!
    riskLevels: RiskLevel!
    actuals: Actual!
    lastUpdatedDate: String
    url: String
    metricsTimeseries: [Metric!]
    actualsTimeseries: [Actual!]
    riskLevelsTimeseries: [RiskLevel!]
  }

  type State {
    fips: String!
    country: String!
    state: String!
    population: Int!
    metrics: Metric!
    riskLevels: RiskLevel!
    actuals: Actual!
    lastUpdatedDate: String
    url: String
    metricsTimeseries: [Metric!]
    actualsTimeseries: [Actual!]
  }

  type Metric {
    testPositivityRatio: Float
    caseDensity: Float
    contactTracerCapacityRatio: Float
    infectionRate: Float
    infectionRateCI90: Float
    icuHeadroomRatio: Float
    icuHeadroomDetails: ICUHeadroomDetails
    icuCapacityRatio: Float
    date: String
  }

  type ICUHeadroomDetails {
    currentIcuCovid: Int
    currentIcuCovidMethod: String
    currentIcuNonCovid: Int
    currentIcuNonCovidMethod: String
    icuCapacityRatio: Float
  }

  type RiskLevel {
    overall: Int
    testPositivityRatio: Float
    caseDensity: Float
    contactTracerCapacityRatio: Float
    infectionRate: Float
    icuHeadroomRatio: Float
    icuCapacityRatio: Float
    date: String
  }

  type Actual {
    cases: Int
    deaths: Int
    positiveTests: Int
    negativeTests: Int
    contactTracers: Float
    hospitalBeds: HospitalBed
    icuBeds: ICUBed
    newCases: Int
    date: String
  }

  type HospitalBed {
    capacity: Int
    currentUsageTotal: Int
    currentUsageCovid: Int
    typicalUsageRate: Float
  }

  type ICUBed {
    capacity: Int
    currentUsageTotal: Int
    currentUsageCovid: Int
    typicalUsageRate: Float
  }
`

I went all out here on models. I left very few keys out and got a lot of great information here. Start small and incrementally add-in types if this is a lot. Or get it all done in one pass.

Go for it all

Update Resolvers to Resolve Correctly

As part of any of our query functions, there is a list of inputs provided to each query.

fieldName: (parent, args, context, info) => data

We don't need parent or info for our functions but what we do need is access to the input args or arguments and the context because that includes our dataSources nested inside our defined Apollo GraphQL server.

// resolvers.js
Query: {
    counties: (_, __, { dataSources }) => dataSources.covidApi.getAllCounties(),
    county: (_, { fips }, { dataSources }) => dataSources.covidApi.getCounty(fips),
    states: (_, __, { dataSources }) => dataSources.covidApi.getAllStates(),
    state: (_, { stateCode }, { dataSources }) => dataSources.covidApi.getState(stateCode),
},

Validate

Once you've wired up your newly created REST Data source to resolvers, you should be able to test in the playground.

Validate a few queries

Faking a Relationship Through API Calls

Let's say the consumers of this API are building a US map showcasing a state and it's counties impact from Covid-19. They want both a US state and all counties to render their UI. If this isn't already a REST endpoint, it is two endpoints: the /state/OH and the /counties endpoints. Let's combine these to push off the lifting to GraphQL and away from the client-side.

Add County Array to State

// schema.js
const typeDefs = gql`
  type State {
    counties: [County!] 
    fips: String!
    country: String!
    state: String!
    population: Int!
    metrics: Metric!
    riskLevels: RiskLevel!
    actuals: Actual!
    lastUpdatedDate: String
    url: String
    metricsTimeseries: [Metric!]
    actualsTimeseries: [Actual!]
  }

Get Counties in getState

// CovidActNowAPi.js

  async getState(stateCode) {
    const state = await this.get(
      `state/${stateCode}.timeseries.json?apiKey=${process.env.COVID_ACT_NOW}`
    );
    const counties = await this.get(`counties.json?apiKey=${process.env.COVID_ACT_NOW}`);

    return {
      ...state,
      counties: counties.filter(({ state: stateCode }) => stateCode === state.state),
    };
  }

If you want you could also add counties to the getAllStates function at this time. Although it won't perform well, it is better here rather than on the client-side.

Laughter

Validate Counties in State

Test out to make sure you can query what you expect.

{
  state(stateCode:"OH"){
    state
    counties{
      fips
      county
      population
      metrics{
        testPositivityRatio
      }
      actuals{
        cases
        deaths
      }
    }
  }
}

Example state including counties

Up Next: Schema First vs. Code First

If you're interested in learning the pro's and con's behind these two GraphQL approaches, stay tuned for my next post where we will take the API written in this tutorial and take it a step further by switching from schema first to code first and scaling up with better developer tooling. I'm kind of winging it on where to go next. If you want to see something specific let me know.

Immediately rethinking my decision

Summary

We have created a GraphQL API from scratch, built a REST service data source to leverage our existing services, and got to utilize the playground to start to see some of those GraphQL perks. If you got lost at any point here is the repository code on the part-1 branch. You also can reference the playground hosted on Heroku to try out some queries.

Old School reference - You're my boy blue!