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_formationInit a git repository:
git initCreate 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/Dockerfilefile, write all the commands to assemble the API image:
In a new
db/Dockerfilefile, write all the commands to assemble the db image:
In a new
docker-compose.ymlfile, declare the two services:
In a new
config.envfile, 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" > .gitignoreIn the
apifolder, interactively create aapi/package.jsonfile:cd api && yarn initAdd
nodemon,babel-cli,babel-plugin-transform-class-properties,babel-preset-flowandbabel-preset-es2015to our dev dependencies:yarn add nodemon babel-cli babel-plugin-transform-class-properties babel-preset-es2015 babel-preset-flow -DIn a new
api/.babelrcfile, write the babel configuration:In our
api/package.json, write the command to launch the server:Create a new empty file
api/index.jsGo 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/shthen inside the container:ls -lath;Exit with:
CTRL-DCHECK 3: You can access the db and prompt the PostgreSQL version:
docker-compose exec db psql -U heroesuser -d heroesdbthen 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-routerIn the
index.jsfile, create our server:
CHECK 1: In your terminal which run docker-compose, you should see
Server is up and runningCHECK 2: Hitting
localhost:3000should 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-bodyparserIn a new folder
api/presentationadd a newschema.jsfile describing a simple graphQL schema:
In the
api/index.jsfile, add ourapiendpoint:
CHECK 1: In Postman, making a POST request to
localhost:3000/apiwhich 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-graphiqlIn the
index.jsfile, let our API knows it should use Koa-graphiql:
CHECK 2: Hitting
localhost:3000/graphiqlshould 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/businessfolder add a newhero.jsfile describing our class for this business object:
In our previous
presentation/schema.jsfile, 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
knexandpgat the root of the project:cd .. && yarn add knex pgAt the root of our project, add a
knexfile.jsfile:
Create a migration file:
yarn knex migrate:make add_heroes_tableand complete the new created file with this:
Create a seed file:
yarn knex seed:make heroesand 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
Heroestable:docker-compose exec db psql -U heroesuser -d heroesdbthen 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
knexandpgin our API:cd api && yarn add knex pgIn the
api/dbfolder add a newindex.jsfile:
In a new
api/db/queryBuilderssubfolder, create a newhero.jsfile and add these few methods to query our data:
Modify the
api/db/queryBuilders/hero.jsfile 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_enemiesComplete the newly created migration file with this:
Modify our
api/db/seeds/heroes.jsseeds:
Run these migrations:
yarn knex migrate:latest && yarn knex seed:runIn our business layer, modify
api/business/hero.jsthis 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 dataloaderAdd a
getLoadersmethod to ourapi/business/hero.jsfile in our business layer:
In our
api/index.jsfile, add a new dataloader to our context for each query on/apiroute:
Back in our
api/business/hero.jsbusiness layer file, modifyloadandloadAllmethods 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.jsfile, add these two methods to parse Authorization header and verify token:
In our
api/index.jsfile, 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/apiwhich 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/apiwhich 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/apiwhich content-type is JSON(application/json) and Authorization Header isBearer authorizedwith 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/apiwhich content-type is JSON(application/json) and Authorization Header isBearer authorizedwith 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.jslike this:
CHECK 1: In Postman, making a POST request to
localhost:3000/apiwhich content-type is JSON(application/json) and Authorization Header isBearer authorizedwith the following raw body:...should return Batman (as
h1) then Superman (ash2).
Next steps
Last updated

