GraphQL, is a technology for fetching and mutating data that makes you wonder why you were building your servers to be REST endpoints for so long. In case you are just meeting it for the first time, grapQL is a runtime and query language that we can use to describe a format for our data and how to get that data. GraphQL is not tied to any specific programming language or database and as such it can be used with any database or language of your choosing so you don't need to learn anything from scratch. GraphQL is just a technology that bridges different parts of your application, this image can give you a hint of what i mean.
You can watch this short video to understand more about graphQL For this article i'll talk about how to create a and configure a basic node js GraphQL endpoint that we can make queries to. I'll be using typescript with nodejs on the server, you can find the tsconfig and package.json files here. If you are superhero developer you have gotten the above files, do store them inside a folder that will serve as the project's directory. Open up that directory in your text editor and let's dive in...
Index
If you have that project set up you can run npm install
to get the dependencies for the project. Doing it manually you'd had to first;
Run
npm i graphql apollo-server
to install apollo-server and graphql for us.Then we'd install TypeScript and nodemon
npm i -D typescript nodemon
.change the main script in
package.json
to point to our js file"main": "dist/index.js",
Add the following to our scripts object still inside the
package.json
file"server": "nodemon dist/index.js"
.Generate a
tsconfig.json
file usingtsc --init
and ensure it looks like this;
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es2016", // or newer if your node.js version supports this
// "strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"moduleResolution": "node",
"strictNullChecks": false,
"resolveJsonModule": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"declaration": false,
"noFallthroughCasesInSwitch": true,
"composite": false,
"noImplicitAny": true,
"lib": [
"dom",
"es2016",
"esnext.asynciterable"
],
"sourceMap": true,
"emitDecoratorMetadata": true,
"strict": false,
"experimentalDecorators": true,
"outDir": "dist",
"rootDir": "src",
}
}
- Create a folder inside a directory like so
/scr/index.ts
. Ensure that/scr
sits at the root level
Inside the the index.ts
we will import ApolloServer
and gql, gql
will help us to compose our schema, define our queries and mutations. ApolloServer
will allow us create an instance of an apollo server that we can make graphQL queries to. Open up the /src/index.ts
and let's code up;
I'll be using firestore for the database, I already have a firebase project set up I will be using the admin sdk. I won't go into setting up a firebase project here because that would take the wind out of our sails.
//index.ts
// * Importing our firebase-admin
import admin from 'firebase-admin'
// * Importing our serviceAccounnt
import serviceAccount from './serviceAccount.json'
// * Importing our apollo-server
import { ApolloServer, gql, ApolloError } from 'apollo-server'
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "firestore Database url"
});
const db = admin.firestore()
Let's familiarize ourselves with what a schema is before we go into creating one.
A Schema is just what it is, graphQL is a strongly typed language like TypeScript. You can compose types from the built in graphQL types that will form a representation of your data. The language is flexible enough to let you to define relationship between your schemas which will enable you build complex schema for your data. Let's define a basic schema for an User and a channel, this will give us a basic intro and understanding of how graphQL schema works.
// /src/index.ts
// Skipped
const typeDefs = gql`
"This will provide information about what you want to describe e.g a User"
# graphQL treats anything that begins with a # as a comment
"An User Schema another comment"
type User{
email: String!,
id: ID!,
name: String!,
channels: [Channel!]!
# A user can have many channels
}
"Describes what a channel looks like"
type Channel {
animal: String!,
id: ID!,
photoUrl: String!,
title: String!
}
`
We have defined a basic Schema for both an User and a Book. Let's take time to pick apart the above schema and see what there is to it. The type User
refers to an Object Type which is one of the graphQL built in types this type allows us to build custom schemas. Some other built in graphQL types include (String, Int, Float, Boolean and ID) they are known as
Scalar
types, the User has an email and name property which are strings. The User has an id property which is of type ID, this is type that specifies a unique identifier, then there's a channel field which is an array of Channel, another type we defined. The exclamation marks are just there to ensure that graphQL doesn't return null to us. The Channel type has it's own schema which tells graphQL how to figure out the structure of what a channel looks like this will be useful when we will compose our queries. This is what makes graphQL so cool, when we query for the User, and we want to get his channels, we also get access to the properties on the Channel Schema, so we can return an array of only the channel names, and or more properties on each channel.Queries
Inside our typeDefs
we will define an additional type called Query
, this is an object that defines the queries we can makes based on the types we defined above, let's see a code example;
// src/index.ts
// skipped
const typeDefs = gql`
type User {
// Skipped
}
type Channel {
// Skipped
}
type Query {
user(id: String!): User, // We can query a user by their id
users: [User!]!, // We can query for all the users
channels: [Channel!]! // We can query for all the channels
}`
When you make queries to this endpoint you can get;
- A single user with their id.
- A list of all the users, this will return to us an array of Users.
- A list of all the channels, will return an array of Channels.
Resolvers
We are primarily done with our type definitions, let's look at resolvers. It is inside the resolvers that we get the actual data and map them to the types in our type definition. Let's dive in; You can resolve an entire type or you can just a property on the type, in this case we will resolve only the user, how to fetch a user based on their ID and how to fetch the list of channels that a user belongs to. We will also resolve the list of channels, the takeaway is that you can resolve your types but you must resolve your queries.
// src/index.ts
// src/index.ts
// skipped
const typeDefs = gql`
type User {
// Skipped
}
type Channel {
// Skipped
}
type Query {
// Skipped
}`
const resolvers = {
// Let's resolve the channels list on the user
User {
// we can customize the atrribute or logic for getting each field on the types
// we defined above, in this case we are only interested in the channels
async channels (parent:any) {
// the parent refers to an individual instance of a user
// Get a reference to the channels collection
const chanRef = await db.collection('channels').get()
const channels = chanRef.docs.map(d => d.data() )
// create an empty array
const userChan:any[] = []
// loop through the user's channels id
parent.channels.forEach((chan:any) => {
// search the channels collection for the channel with an id that
// matches the id we are iterating over
const channel = channels.find((item:any) => chan == item.id)
// add that chanel to the array of users channel
userChan.push(channel)
})
return userChan
}
},
// Let's resolve our Query
Query: {
// remeber the Query we defined in typeDefs, this is for a list of channels
channels: async (parent, args) => {
// Basic firebase
const channelsRef = await db.collection('channels').get()
return channelsRef.docs.map(c => c.data())
},
// this is for a list of users
users: async (parent, args, context) => {
try{
// Basic firebase stuff
const usersRef = await db.collection('users').get()
return usersRef.docs.map(user => user.data())
}
catch(err) {
console.log(err)
return new ApolloError(err)
}
},
// an individual user, when we want to query for a user, we can pass in
// an id as an argument, it will be added to args object but we are destructuring
user: async (parent:any, {id}: any, context: any) => {
// Basic firebase
const userRef = await db.collection('users').doc(id).get()
return userRef.data()
}
}
}
Launching our Apollo Server
Now we just need to launch our server, to do that we create a new instance of an ApolloServer
and pass it an object that contains the typeDefs and resolvers we defined above. We then call listen on the server like we would on an express server. Don't forget to compile it to JavaScript since we are using TypeScript for this project.
// src/index.ts
// src/index.ts
// skipped
const typeDefs = gql`
type User {
// Skipped
}
type Channel {
// Skipped
}
type Query {
// Skipped
}`
const resolvers = {
// Let's resolve the channels list on the user
User {
// Skipped
},
// Let's resolve our Query
Query: {
// skipped
}
}
const server = new ApolloServer({ typeDefs, resolvers })
server.listen().then(({ url }) => {
console.log(`Server running on ${url}`)
})
And that's our simple graphQL server set up, you can install the graphQL playground extension on chrome, copy the url from your terminal and paste in the url and test your Schema, just write your Schema inside and run it;
// example playground to test api
Query {
users {
name,
email,
channels {
title,
animal
}
}
}
Run the query, you should see a list of users with channels that they are subscribed to. We will touch up mutations later, hope this gets you on your way to graphQL server. If all of this felt like junk then try watching video by Jeff on building an Apollo Server and I have to admit, it was the inspiration for this post. That being said i hope you found this useful and learned something from this.