getting-started-with-apollo-server-dataloader-knex.mo

Prerequisites (~12min)

  • Have Yarn installed (~5min)

  • Have Docker and Docker-compose installed (~5min)

  • Have Postman installed (~2min)

Thanks

Thanks to Tycho Tatitscheff and Yann Leflour for helping me with their great BAM API repo

Context

During this standard, we will create a Heroes graphQL API. We will have a Hero model with superheroes real and hero names. We will add one example of association.

Our API will be lightly protected and use batching to minimise DB round-trips.

Steps (~61min)

Note: You should commit between each step.

Initialise a new project (~6min)

  • Create and go to a new directory for the project: mkdir graphql_formation && cd graphql_formation

  • Init a git repository: git init

  • Create two services with Docker-compose, one postgres database and one node server:

    • For this step, notice that our final folder architecture looks like this:

    • Make sure your local 3000 port is available as we will use this port to reach our API

    • In a new api/Dockerfile file, write all the commands to assemble the API image:

    • In a new db/Dockerfile file, write all the commands to assemble the db image:

    • In a new docker-compose.yml file, declare the two services:

    • In a new config.env file, declare your environnement variable for these Docker containers:

  • Build these services with the command: docker-compose build

CHECK 1: Your terminal should prompt successively these lines confirming Docker images have been built:

Successfully tagged heroes-db:latest

Successfully tagged heroes-api:latest

Install nodemon and run our project (~5min)

  • Add this to the project .gitignore: echo "node_modules" > .gitignore

  • In the api folder, interactively create a api/package.json file: cd api && yarn init

  • Add nodemon, babel-cli, babel-plugin-transform-class-properties, babel-preset-flow and babel-preset-es2015 to our dev dependencies: yarn add nodemon babel-cli babel-plugin-transform-class-properties babel-preset-es2015 babel-preset-flow -D

  • In a new api/.babelrc file, write the babel configuration:

  • In our api/package.json, write the command to launch the server:

  • Create a new empty file api/index.js

  • Go back to the root of the project: cd ..

  • Run the project: docker-compose up

CHECK 1: You terminal should prompt the logs of the two containers together with two different colors

CHECK 2: From another terminal, you can access the API and see the following folder structure: docker-compose exec api /bin/sh then inside the container: ls -lath;

Exit with: CTRL-D

CHECK 3: You can access the db and prompt the PostgreSQL version: docker-compose exec db psql -U heroesuser -d heroesdb then inside the container: select version();

Exit with: CTRL-D

Create a koa server (~3min)

  • Install koa and koa-router in our API: cd api && yarn add koa koa-router

  • In the index.js file, create our server:

CHECK 1: In your terminal which run docker-compose, you should see Server is up and running

CHECK 2: Hitting localhost:3000 should return Hello World!: curl localhost:3000

Create a presentation layer with graphQL (~6min)

This layer will let our API know how to present data: what data one user can query? How should front end query this data (fields, root queries, sub queries...)?

  • Install graphQL, graphQL Server Koa, graphQL tools and Koa body-parser: yarn add graphql graphql-server-koa graphql-tools koa-bodyparser

  • In a new folder api/presentation add a new schema.js file describing a simple graphQL schema:

  • In the api/index.js file, add our api endpoint:

CHECK 1: In Postman, making a POST request to localhost:3000/api which content-type is JSON(application/json) with the following raw body:

...should return our two heroes, Clark and Bruce:

  • Install Koa graphiQL: yarn add koa-graphiql

  • In the index.js file, let our API knows it should use Koa-graphiql:

CHECK 2: Hitting localhost:3000/graphiql should return graphiql interface and show the Docs

CHECK 3: Using graphiql interface with the following query:

...should return our two heroes, Clark and Bruce:

Create a business layer (~5min)

This layer will contain all business logic: access controll, scoping / whitelisting, batching and caching and computed properties. More explanations can be found here, in the bam-api repo. In this MO, we will only cover access control logic and batching / caching.

  • In a new api/business folder add a new hero.js file describing our class for this business object:

  • In our previous presentation/schema.js file, modify our mocked resolvers to use our business layer:

CHECK 1: Using graphiql interface with the following query:

...should return our two heroes, Clark and Bruce.

CHECK 2: Using graphiql interface with the following query:

...should return Clark Kent with its id: 1.

CHECK 3: Using graphiql interface with the following query:

...should return Bruce Wayne with its id: 2.

Seed our database (~8min)

  • Install knex and pg at the root of the project: cd .. && yarn add knex pg

  • At the root of our project, add a knexfile.js file:

  • Create a migration file: yarn knex migrate:make add_heroes_table and complete the new created file with this:

  • Create a seed file: yarn knex seed:make heroes and complete the new created file with this:

  • Run the migration and the seed: yarn knex migrate:latest && yarn knex seed:run

CHECK 1: You can access the db and prompt content of the Heroes table: docker-compose exec db psql -U heroesuser -d heroesdb then inside the container: select * from "Heroes";;

Exit with: CTRL-D

Create a db layer with knex (~6min)

This layer let our API query the data using knex query builder.

  • Install knex and pg in our API: cd api && yarn add knex pg

  • In the api/db folder add a new index.js file:

  • In a new api/db/queryBuilders subfolder, create a new hero.js file and add these few methods to query our data:

  • Modify the api/db/queryBuilders/hero.js file in our business layer this way:

CHECK 1: Using graphiql interface with the following query:

...should return Clark Kent with its id: 1.

CHECK 2: Using graphiql interface with the following query:

...should return all 4 heroes of our database.

Add association to our API (~6min)

Association are made both in our db and in our API, in our presentation layer.

  • Create a new migration: cd .. && yarn knex migrate:make add_heroes_enemies

  • Complete the newly created migration file with this:

  • Modify our api/db/seeds/heroes.js seeds:

  • Run these migrations: yarn knex migrate:latest && yarn knex seed:run

  • In our business layer, modify api/business/hero.js this way:

  • In our API, in our presentation layer, modify our api/presentation/schema.js:

CHECK 1: Using graphiql interface with the following query:

...should return Clark Kent with its heroName and its enemy: Batman.

Push your API to the next level: use caching with Dataloader (~6min)

Trying to query heroes and their enemies'heroName will show up a N+1 problem. Indeed, our API make 5 round-trips to our database! Try yourself:

We can reduce these calls adding caching to our business layer

  • Install Dataloader: cd api && yarn add dataloader

  • Add a getLoaders method to our api/business/hero.js file in our business layer:

  • In our api/index.js file, add a new dataloader to our context for each query on /api route:

  • Back in our api/business/hero.js business layer file, modify load and loadAll methods to use our dataloader:

  • Protect loader.load() function call if no argument is supplied:

CHECK 1: Using graphiql interface with the following query:

...should return all heroes and their enemies and your terminal should prompt only one request to the DB.

CHECK 2: Using graphiql interface with the following query:

...should return Clark Kent and Bruce Wayne; and only one SELECT call should have beeen made to our DB.

Add access control to our API (~5min)

This is a very simple example, for a more advanced solution, prefer using Koa Jwt.

  • In a new api/utils.js file, add these two methods to parse Authorization header and verify token:

  • In our api/index.js file, parse authorization header and pass it to our context:

  • In our business layer, modify api/business/hero.js:

CHECK 1: In Postman, making a POST request to localhost:3000/api which content-type is JSON(application/json) with the following raw body:

...should return UNAUTHORIZED.

CHECK 2: In Postman, making a POST request to localhost:3000/api which content-type is JSON(application/json) with the following raw body:

...should return UNAUTHORIZED.

CHECK 3: In Postman, making a POST request to localhost:3000/api which content-type is JSON(application/json) and Authorization Header is Bearer authorized with the following raw body:

...should return Clark Kent.

Troubleshooting: Accessing data by id in the correct order (~5min)

You should notice that in Postman making a POST request to localhost:3000/api which content-type is JSON(application/json) and Authorization Header is Bearer authorized with the following raw body:

...returns the same than the following request (ids switched):

This is due to our DB query: select * from "Heroes" where "id" in (1, 2) return the same result than: select * from "Heroes" where "id" in (2, 1).

  • In utils.js, add the following method:

  • In our db layer, modify api/db/queryBuilders/hero.js like this:

CHECK 1: In Postman, making a POST request to localhost:3000/api which content-type is JSON(application/json) and Authorization Header is Bearer authorized with the following raw body:

...should return Batman (as h1) then Superman (as h2).

Next steps

  • Add graphiQL with authorization header (get inspired by BAM API)

Last updated