The goal of the workshop is to create your own GraphQL-server that acts as a gateway on top of a few REST-services.
We assume that all participants have a basic idea of GraphQL is and have seen a GraphQL-query before.
In this workshop we will be covering:
- Creating a Hello World GraphQL-app
- Proxying requests from existing REST-service
- Tying different data types in your REST-service together in a graph
- Optional: Batch fetching of resources
- Optional: Creating a little server-rendered React app that uses your service
So. We're building a blog site and we're designing the API for it too.
If you start by installing (npm i
) & running the project (npm run dev
) you can see that the REST service exposes a few endpoints.
When querying http://localhost:3101/posts?_limit=10 we get a list of this sort of objects:
{
"id": "post21",
"title": "Quod maiores eveniet",
"slug": "Et-velit-harum-voluptas-quasi-ad-in-ut.",
"text": "Quod nobis eveniet minima blandit [..]",
"user": {
"id": "user1",
"handle": "Jillian_Brakus24"
}
}
And when querying http://localhost:3101/users we get this sort of objects:
{
"id": "user1",
"handle": "Jillian_Brakus24",
"firstName": "Jillian",
"lastName": "Brakus",
"url": null,
"job": {
"title": "District Communications Supervisor",
"area": "Operations",
"type": "Architect",
"companyName": "Homenick, Leffler and Shanahan"
}
}
Let's say this was the agreement of exactly what information was needed when the page was designed. We wanted to be able to display a list of posts, and together with the post we wanted to display the user's handle
. Great! This works fine.
However, with time we revise the design and decide we also want to display users' firstName
& lastName
in the list instead of the handle.
We have a few options:
- Add
firstName
andlastName
to/posts
- π· Increases payload for all consumers. Maybe you have a mobile app that doesn't need this, why should those users be punished with a slow experience?
- π· Requires additional work on the backend - the data is already there!
- Build a
/v2/posts
-endpoint or specific endpoint for this client- π· Maintain new and old endpoints
- π· Doesn't solve same problem in the future
- Add something like dynamic
?fields=..
param to specify which fields you want to return- π· Not part of REST-spec
- π· Quite difficult to structure and write good resolvers
- π· Hard to test properly and quite messy to dynamically build this query string
- π· Does not work nicely when there's complex structures like arrays or nested objects
- Request
/user?=id=..
for each user in posts- π· Extra roundtrips to server. Have to fetch requests posts, and then each user. Inefficent.
- π· Over-fetching: you'll get more data than you actually need on the users. Slow.
- π· A lot of glue code on the clients to put the objects together
In GraphQL, you define your types and their relationships on a graph and let the consumer request exactly what data they need, and they'll receive just that - no more, no less.
Assume you have implemented a GraphQL-server with this schema:
type Query {
posts(limit: Int page: Int) [Post!]!
}
type Post {
id: String!
title: String!
slug: String!
text: String!
user: User!
}
type User {
id: String!
handle: String!
url: String
firstName: String!
lastName: String!
}
You can then let the client request exactly what they want with a query like this:
query {
posts(limit: 1) {
id
title
slug
text
user {
id
firstName
lastName
}
}
}
This would give the following output:
{
"data": [
{
"id": "post21",
"title": "Quod maiores eveniet",
"slug": "Et-velit-harum-voluptas-quasi-ad-in-ut.",
"text": "Quod nobis eveniet minima blandit [..]",
"user": {
"id": "user1",
"firstName": "Jillian",
"lastName": "Brakus"
}
}
]
}
Isn't that nicer? You can define your data requirements in your client rather than hardcoding them in the API.
git clone git@github.com:KATT/graphql-workshop.git
cd graphql-workshop
npm i
npm run dev
.
|-- graphql: graphql service (this is the main working directory)
| |-- src
| | |-- index.js: entry point of app, starts server
| | |-- server.js: graphql server. β where we'll be writing all our logic
| | `-- schema.graphql: graphql definition of our API
| |-- test
| | |-- query: tests for grapql queries
| | | |-- hello.spec.js: tests for `query.hello`
| | | `-- posts.spec.js: tests for `query.posts` β
|-- rest: mock REST service running with resources `posts`, `users`, &, `comments`
| `-- [..]
| | -- [..]
The whole workshop is in TDD-style. We have tests written in graphql/test/query
and your job is to gradually remove .skip()
and make them pass with the help of the paired markdown-file.
- http://localhost:3100 - your GraphQL-server
- http://localhost:3101 - your REST-server
- http://localhost:3200 - your React app
Start by opening http://localhost:3100 (after npm run dev
) to see your GraphQL playground and get familiar with the project.
Try this query:
{
hello(name: "World")
}
This query takes an input argument called name
and outputs it in the results. See in the schema.graphql
& server.js
& how it fits togeter.
This is the equivalent of doing the following query in curl
curl 'http://localhost:3100/' \
-H 'content-type: application/json' \
--data-binary '{"query":"{ hello(name: \"World\") } "}' \
--compressed
It's a simple POST request containg the query
string in a JSON body. All requests follow this structure - it's always a POST to the same endpoint.
To start the test watcher run: npm t -- --watch
Now, head over to graphql/test/query/posts.spec.js
and remove .skip()
from the first integration test.
π Bonus: If you're running vscode and installed the recommended jest plugin you can get your test results in the editor.
What is a post?
type Post {
id: String!
title: String!
# [... TBC]
}
Post
is a GraphQL Object Type, meaning it's a type with some fields. Most of the types in your schema will be object types.id
andtitle
are fields on thePost
type. That means thatid
andtitle
are the only fields that can appear in any part of a GraphQL query that operates on thePost
type.String
is one of the built-in scalar types (together withFloat
/Int
/Boolean
) - these are types that resolve to a single scalar object, and can't have sub-selections in the query. Think of it as a primitive type for now.String!
means that the field is non-nullable, meaning that the GraphQL service promises to always give you a value when you query this field. In the type language, we'll represent those with an exclamation mark.
Take the post definition above and add it to schema.graphql
Look at a post object in http://localhost:3101/posts and add the remaining fields. You can skip user
for now, we will return to that later.
In order to query your API for posts, you need to define a query for posts.
We do that by defining the following on your root type Query
type Query {
# [...other resolvers]
posts: [Post!]!
}
In order to get your GraphQL API to return something based on your definition, you need to define resolvers for your queries.
For starters, in order to fulfill this test, all you need to have is a resolver that returns an array.
In server.js
there's an object of resolvers - on Query
, add a posts
field.
const resolvers = {
Query: {
// [..]
posts: async (source, args) => {
return [];
},
},
};
We want this to do external requests later on (or maybe in another app it'd be a DB query), so and async
function is preferred.
The REST-API returns an endpoint, /posts
, where you can fetch all post resources.
Here's a tip:
await request({
uri: `${REST_SERVICE_URL}/posts`,
json: true,
});
return posts;
You probably want to limit the results & you don't want to do that on the GraphQL-side, it's nicer to just pass on the limit to the underlying service and make use of it's pagination.
In order to do this you need to build up a query string.
The endpoint you want to query is the following: http://localhost:3101/posts?_limit=2
First, you need to add to your GraphQL-schema that you want to accept arguemnt limit
and which type it is.
posts(limit: Int): [Post!]!
Secondly, you want to use this argument to build up a query in the resolver.
const query = {};
if (args.limit) {
query._limit = args.limit;
}
const search = `?${querystring.stringify(query)}`;
const posts = await request({
uri: `${REST_SERVICE_URL}/posts${search}`,
json: true,
});
- Define a
User
type - Define the relation between
Post
andUser
(User!
)
Since the /posts
endpoint don't contain the users' firstName
s, you need to resolve the related /users?id=x
when you resolve a post.
In your resolvers, you need to add a Post
resolver, like this
Post: {
user: async (post, args, ctx) => {
// ..
The first argument when resolving any field on a type will be the source object, in this case, your post. You can use this to fetch the user on /users/${post.user.id}
.
It is quite inefficent to do one user request per post & since our REST-API allows us to batch fetch users based on their ids, we'd like to leverage that.
We are able to fetch several users at once by calling /users?id=user1&id=user2&..
.
There's a library called DataLoader which has a neat approach to this sort of problem. Basically, you create a dataloader where you define how to batch fetch objects based on a list of identifiers and then you use said data loader to load all of your objects. If the same id has already been fetched, it's simply returned or otherwise it will be fetch in the next batch request.
When we define our GraphQLServer
we can define a context
object. The context we create is unique for each request to our API, hence we can create a cached dataloader here that will exist only for this request.
The third argument in our resolvers are always said context object, and we can use this to call our loader (ctx.userById.load(post.user.id)
).
Once you've done the above you should be equipped with knowledge to define your GraphQL-schema and write your own resolvers.
- Make it into an actual blog! Try to add the queries so that
web/
works (server-rendered React app using Next.js) - Add
limit
andpage
for pagination. Comment
<->Post
relationship + resolvers- Ability to add posts / comments (Mutations)
- Try deploying your services using now.sh
npm i now -g
cd rest
now
First, get the URL of your REST-service
Then,
cd graphql
now -e REST_SERVICE_URL=https://...something.now.sh
Get the URL from your GraphQL-service.
cd web
now -e GRAPHQL_URL=https://...something.now.sh