GraphQL has been adopted in many products after Facebook open-sourced it in 2015. We, at Dockup lately changed our APIs to GraphQL service, and that was my job.
I thought it must be way more complex to implement a full GraphQL service for our app, but surprisingly it was easy and fun.
Thinking about how to convey what I learned with the same interest and fun, I concluded, learning by building an app will be great. So, Yeah! we'll build a small app for restaurants to add, view and delete menu items, allowing us to deep dive into different GraphQL concepts.
I couldn't do all of these in one post, so there will be a series of them:
- Intro: you can go through my another post for some intro (optional)
- Implementation with NodeJs as server
- Implementation in Elixir with Absinthe GraphQL and Phoenix framework
- React front end app connected to GraphQL
Setup
Let's start by setting up the project directory layout:
Add the packages we need:
Add this to your package.json for nodemon
to sync with the changes
And run yarn start:dev
in your terminal.
We can use the apollo-server
package, which is an abstract wrapper over the graphql
package.
We'll use an Array
as a data source for now and connect the database later.
Query
In GraphQL, you can consider Query
as a set of user-defined functions that can do what GET
does in REST.
Let's assume we want to get this data:
Let me write a basic index.js
setup for a query and explain it:
Okay, what's going on?
First, we are getting the services we need, ApolloServer
and gql
.
- We instantiate ApolloServer to get the server running and interact with it
gql
parses the GraphQL string which JavaScript doesn't support internally.
TypeDefs
typeDefs is a GraphQL schema.
A GraphQL schema is written in its own typed language: The Schema Definition Language (SDL)
We define the queries a client can do in Query
type (here menuItems) and the
mutations a client can do in Mutation
type (we'll see later).
A type
is nothing but an object containing fields, where keys are the Field names
and
values are the data-type
of the Field. In GraphQL, every field should be typed.
And the strings you see above the fields are documentation strings. You can see the docs
on the graphiql
interface which we'll talk about later.
In GraphQL, data types are:
- Scalar types:
- Int
- String
- Float
- Boolean
- ID: Represents unique number, like id in db.
- Object types
- type: In the above example we are defining MenuItem as a type and use that as the data-type for menuItems field in Query type
Types can be represented as:
- [typeName]: Array of data with the type
typeName
should be returned - typeName!: the
!
in the typeName represents nonNullable i.e, the returning field should not be null - You can also combine these representations like [typeName!]! i.e, you should return a nonNull
array with nonNull elements which matches
typeName
type
Resolvers
Resolver map relates the schema fields and types
to a function it has to call to
turn the GraphQL operations into data by fetching from any source you have. For now, it's just an array for us.
Let us add another field to menuItem
: reviews
with its type:
Let's assume there's some array of reviews
which have the menuItem Id as
a Foreign key, then the resolvers will be:
The query
will call Query.menuItems
first and then pass it's returned value as
parent
to MenuItem.reviews
. The result will be:
A resolver can return an Object
or Promise
or scalar
value, this should also match the data-type defined in the schema
for that field. Resolved data will be sent if a promise returned.
Every resolver function takes four arguments when it's called:
-
parent
: Object contains the returned value from the parent's resolver.Every GraphQL query is a tree of function calls in the server. So, every field's resolver gets the result of parent resolver, in this case:
- query aka rootQuery is one of the top-level Parents.
- Parent in Query.menuItem will be whatever the server configuration passed for rootQuery.
- Parent in MainItem.reviews will be the returned value from the resolver Query.MenuItems.
- Parent in Review.id, Review.comment and Review.authorId will be the resolved value of MenuItem.reviews.
-
params
: Object contain the arguments we passed in the query likequery { menuItem(id: 12) { name } }
the params will be{ id: 12 }
-
context
: You can pass an object when instantiating the server and access it on every resolver.Example:
-
info
: This argument mostly contains the info about your schema and current execution state.
Default resolver
Every type need not have a resolver, ApolloServer
provides a default resolver,
that looks for relevant field name in the parent object, or call the function if we have defined one for the field
explicitly.
For the following schema, comment
field of the Review
would not need a resolver if the result
of reviews
resolver returns a list of objects which are already containing a comment
field.
Start the server
Instantiate ApolloServer
with typeDefs
and resolvers
to listen for queries.
Make the query
Go to localhost:4000
, enter the query on the left side of the graphiql
: An interface that is provided by apollo-server to test,
on the right, you can access docs
of different types you have and field infos.
the result will closely match the query by returning only what we asked for:
A query is like a tree of functions which fetches data. For example: If we take the above query,
You can imagine fields as pipes of functions, which could return a data or call another function.
Fields with data-types Int
, String
, Boolean
, Float
and ID
returns a data, but type
fields
will call a function, depends on its fields' data-type, returning a data or function call will happen.
Database setup
We'll use a package called sequelize to use Postgres database with MongoDB like functions. It will also work with any other DB.
Set up a DB and get the URL to pass it as an argument to the Sequelize constructor.
Stop the app and run the below command in your terminal:
We'll use sequelize to connect to database, Model a table, then interact with the table
" GraphQL is not bound to any database, doesn't interact with the database on its own, we have to do all the querying and return the data which a GraphQL query expects. "
Our Postgres database hosted on Heroku.
We'll change the existing code to use sequelize:
Run the query in graphiql
You should get an empty array because we haven't created any menu item in the database yet, we will do that using mutation
Mutations
You can think of Mutations
as user-defined functions which can do what POST
,
PUT
, PATCH
and DELETE
does in REST
Change the TypeDefs to:
Here, we introduced two new fields: Mutation
and input
Mutation field
Like Query
, Mutation
is also a special field where we will define all mutations
(the fields inside Mutation
type), we will also write resolver functions
for our mutations.
Input Objects
Input fields are also like type
fields, but this defines types for the
arguments to be passed by the client query if it is an object.
For example, the query to create a menuItem:
See, we are passing an object menuItem
as an argument to the addMenuItem
mutation, like a function.
This menuItem
should match the input MenuItemInput
type we defined.
Resolver function
Sequelize functions will always return a promise, so we are returning promises returned by the functions.
Run this query:
The menu item will be created in the table using the resolver function and the value returned for the query will be:
Then run this query with the returned menuItemId:
The result would be:
Did you notice that even though we didn't define id
in the models we got the result with
id
? It is because sequelize automatically added it to the table when creating the record.
We can also pass arguments as individual values,
It's all about how we get the values from parameters in the resolver function.
That's it from my side. You have exercises on creating a query getSingleMenuItem
and a mutation deleteMenuItem
.
See you in the next post: How to implement these with Elixir and Phoenix.
Good Luck! 😇