Creating a GraphQL Server with Apollo
To create an GraphQL server using Apollo Server we need to do three things:
- install
apollo-server
andgraphql
- define a schema
- define resolvers
Let's install the needed dependencies:
npm install --save apollo-server graphql
Once we have done that we are good to proceed with the next steps.
Define a schema
To define a schema we need to be using a function called gql
that allows us to define a schema like a multiline template string and most importantly using the GraphQL query language.
Let's first look at how to import gql
:
const { gql } = require("apollo-server");
Next up we can start defining the schema itself:
const typeDefs = gql`
type Query {
products: [Product]
product(id: Int!): Product
}`
type Product {
id: Int,
name: String,
description: String
}
input ProductInput {
name: String,
description: String
}
type Mutation {
login(email: String): String # token
createProduct(product: ProductInput)
}
Lastly we export the schema for later usage
module.exports = typeDefs;
The full file should now look like this:
const { gql } = require("apollo-server");
const typeDefs = gql`
type Query {
products: [Product]
product(id: Int!): Product
}`
type Product {
id: Int,
name: String,
description: String
}
input ProductInput {
name: String,
description: String
}
type Mutation {
login(email: String!): String # token
createProduct(product: ProductInput): String
}
module.exports = typeDefs;
Define resolvers
Next up we need to define what the following queries and mutations should do:
- products, this should return a list of products
- product(id: Int!), this should return one product
- login(email: String), this should log in the user
- createProduct(product: ProductInput), this should create a product
Let's start a new file resolvers.js
. At this point we will keep things simple and have some local static data but we could improve this server by having it call a database or an API. So let's define some static data at the top:
const products = [];
Now that Apollo server expects a certain type of object back with the following shape:
module.exports = {
Query: {},
Mutation: {}
};
Not the two keywords Query
and Mutation
above. These two are objects and we need to define each corresponding resolver function. Let's start by adding a resolver function for products:
const getProducts = () => {
return Promise.resolve(products);
}
module.exports = {
Query: {
products: async () => getProducts
},
Mutation: {}
};
A comment on getProducts()
method is that we return a Promise
. Now, we could be return our products
list directly but this is just a way to showcase that we can be returning asynchronous response as well which means this will fit well with us talking to a 3rd party API or doing a lookup towards a database. To go along with our Promise
response we mark it with async
in our return object.
Define a resolver function with a parameter
Ok so we've learned how to define a resolver function but let's have a look next how we can grab a parameter from the user request and query our backend based on it.
const getProductById = ({ productId }) => {
return Promise.resolve(product.find(p => p.id === productId));
}
module.exports = {
Query: {
products: async () => getProducts,
product: async (_, { id }) => getProductById({ productId: id })
}
};
Above we added product
to our Query
object and as you can see we can quite easily dig out the id
from the user request.
Define a resolver function for a mutation
To define resolvers for mutations we need to introduce a Mutation
property on our exported object, just like we needed to introduce Query
. So now our code should look like this:
module.exports = {
Query: {
products: async () => getProducts,
product: async (_, { id }) => getProductById({ productId: id })
},
Mutation: {}
};
Note the addition of Mutation
above.
Net step is to add our createProduct
property and a resolver function that handles that, like so:
const createProduct = ({ product }) => {
const newId = products.length === 0 ? 1: products[products.length-1].id + 1;
products = [ ...products, { ...product, id: newId}];
return Promise.resolve('success')
}
module.exports = {
Query: {
products: async () => getProducts,
product: async (_, { id }) => getProductById({ productId: id })
},
Mutation: {
createProduct: async (_, { product }) => createProduct({ product })
}
};
Putting it all together
Now we have defined a set of resolver functions, let's have a look at the resulting code in its entirety:
const products = [];
const getProducts = () => {
return Promise.resolve(products);
}
const getProductById = ({ productId }) => {
return Promise.resolve(product.find(p => p.id === productId));
}
const createProduct = ({ product }) => {
const newId = products.length === 0 ? 1: products[products.length-1].id + 1;
products = [ ...products, { ...product, id: newId}];
return Promise.resolve('success')
}
module.exports = {
Query: {
products: async () => getProducts,
product: async (_, { id }) => getProductById({ productId: id })
},
Mutation: {
createProduct: async (_, { product }) => createProduct({ product })
}
};
As you can see we have only operated on static data but we could easily switch those calls to target a database or a 3rd party API, that's entirely up to you. As long as you remember to use the await
keyword in your resolver definition you are good to go with interacting with anything return a Promise
Start the server
Ok we have now defined a schema and a set of resolver functions. Now let's import schema.js
and resolvers.js
and kick off our server:
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
Debug
At this point, navigate to http://localhost:4000
,
you should have something looking like this:
As you can see this looks very much like the GraphiQL environment we presented in the chapter GraphQL + Node.js Express. The idea is the same. On your left you have an area where you can enter
queries
and mutations
and on your right is the results area. On the bottom left is variables
area that allows you to specify input variables that you can use the top left pane.
Example usage
Let's try to create a query
. We can do so in two ways:
- using the no-keyword version,
{}
- using they keyword
query
The first variant looks like this:
As you can see above we are only using
{}
to specify our query and then we query for a resource launches
and drills down into the resource to get the columns that we need, i.e mission
and under mission
we grab the column name
.
Let's try to use the other query variant next by using the keyword query
. The keyword query allows us to:
- Name the query so we can refer to it later
- Specify more than one query in the same window, so we can choose which one we want at a given point in time
- Specify a parameter for our query ( we will show this one in our next example )
Above we managed to specify two different queries and given them the name
LaunchesNameOnly
and LaunchesAll
. While specifying the LaunchesAll
query you can see how our environment helps us by indicating the type of isBooked
, that we are about to select. The type is a Boolean!
, a mandatory Boolean
.
In our next example we show how you hit the largw play
button and is given the choice between different queries, as you can see it is a really great idea to define your queries with the keyword query
so to create some order in your testing environment:
Below is simply the result of executing our query
Let's try to use variables next and see how we can declare those in the bottom left area and use them in our queries
and mutations
.
Above we have defined
OneLaunch
that takes a parameter $id
. Thereafter this parameter $id
is passed down to our actual query expression like so:
launch(id: $id) {
mission {
name
}
}
Now, we are ready to invoke our query, with a parameter and we simply hit the play button and we get this:
The reason we get the above is that it read from the bottom left are that looks like this:
Variables in this area is all made up of one object and we simply add keys to this object when we need a new parameter like so:
{
"id": 1,
"otherparam":"some value"
}
deeper queries
One of the big selling points of GraphQL is the ability to query deep into a hierarchy, like so:
{
orders {
created,
createdBy {
name,
created
},
items {
product {
name
},
price
}
}
}
Above we are querying on the resource orders
and as you can see we are able to perform a lot of subqueries and query for orders->items->product
. Were we to do that with SQL it would be quite difficult with a lot of JOINS
. So how are we able to construct this in GraphQL?
In short, we need to do two things:
- define a resolver to the asked for subtype
- store the data in a resolvable format. This means we need to store the data as an id/ids rather than the full object
Store the data as id/ids
We need to store an order like this:
const orders = [{
id: 1,
items: [1,2,3]
}]
Define a resolver for a subtype
How this is done syntactically depends on the GraphQL server implementation we are dealing with. In this case we are dealing with Apollo, so let's look at how Apollo deals with it. We know that when we define a resolver object in Apollo it looks like this:
{
Query: {
orders: async () => getOrders(),
order: async (_, { id }) => getOrder(id)
},
Mutation: {
...
}
}
Now, our querying for orders
means we are interested in querying for all it's columns like created
but also for a complex column like items
and even items=> product
so we can ask a query like this:
{
orders {
created,
items {
product {
name
},
price
}
}
}
So how do we resolve items
?. Well let's have a look at our schema definition of an order:
type Order {
created: Date;
items: [Item]
}
With this in mind let's head back to our resolver object and now we will add a Order
type to like so:
{
Query: {
orders: async () => getOrders(),
order: async (_, { id }) => getOrder(id)
},
Mutation: {
...
},
Order {
items: ({ items }) => getItems(items)
}
}
Let's zoom into our addition:
Order {
items: ({ items }) => getItems(items)
}
Here we can see that we are resolving what happens if someone queries for items
. We are missing the getItems()
method though so let's define that:
const itemsList = [{
id: 1,
name: 'item'
},
{
id: 2,
name: 'item2'
}]
const getItems = (items) => {
return Promise.resolve(itemsList)
}
Ok, so that deals with items
so how do we deal with items->product
. Well, that one is quite simple, we just again look at our schema:
type OrderItem {
price: Double;
product: Product;
}
We see now that we need to add OrderItem
to our resolver object, like so:
{
Query: {
orders: async () => getOrders(),
order: async (_, { id }) => getOrder(id)
},
Mutation: {
...
},
Order {
items: ({ items }) => getItems(items)
},
OrderItem {
product: ({ product }) => getProduct(product)
}
}
Summary
We have covered
- Installing
apollo-server
andgraphql
, in order to have the necessary dependencies to create an apollo server. - Defined a
schema
and a set ofresolvers
. - Booted up our server,
- Explored the visual environment that allows us to run queries or mutations
Moving on we will look at different ways to consume said server using apollo-client
so stay tuned for that.