getting-started-with-apollo-server-dataloader-knex.mo
Owner: Thomas Pucci
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 aapi/package.json
file:cd api && yarn init
Add
nodemon
,babel-cli
,babel-plugin-transform-class-properties
,babel-preset-flow
andbabel-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 returnHello 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 newschema.js
file describing a simple graphQL schema:
In the
api/index.js
file, add ourapi
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:
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 DocsCHECK 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 newhero.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
andpg
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
andpg
in our API:cd api && yarn add knex pg
In the
api/db
folder add a newindex.js
file:
In a new
api/db/queryBuilders
subfolder, create a newhero.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 ourapi/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, modifyload
andloadAll
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 isBearer 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 isBearer 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 isBearer authorized
with the following raw body:...should return Batman (as
h1
) then Superman (ash2
).
Next steps
Last updated