GraphQL: Tutorial
GraphQL is an excellent tool to have when building APIs and saves a lot of development time when working in a team. The front-end can easily keep up with the changes the back-end engineers are making and keeps the team communication strong.
If you’re still wondering if you should be using graphql for your next application, this blog will not be covering all of those reasons or you could just go ahead and trust me to pick it up.
Let’s dive into the walkthrough. We’ll be covering up 5 action items and these are
- Create a NodeJS application
- Setting up a GraphQL Server
- Enhancing Resolvers with Queries & Mutations
- Adding context
- Authorisation
If you want to dive directly into the code then please head over to
Tools of trade
The tools we’ll be using are
- NodeJs (Language of choice)
- Typescript (Super charging javacript)
- Apollo-Server (GraphQL server)
- Type-Graphql (Generates types for apollo server / graphql)
Step 1: Create a NodeJS Application
Assuming you have yarn
installed and know your way around npm
then please start away with the following command in a new folder.
yarn init
yarn add -D typescript
npx tsc --init
This will initiate a new project with typescript settings. Moving on lets update our tsconfig.json
to place in rules to ensure our code is top quality.
yarn add -D tsconfig.json
npx tsconfig.json
The last command will prompt you to select your project type. Select NodeJS.
Next go on to add the following packages
yarn add -D ts-node
yarn add -D nodemon
yarn add -D @types/node
yarn add dotenv
yarn add express
yarn add -D @types/express
- ts-node (Typescript execution)
- nodemon (Restarting node server after changes)
- @types/node (Types for Nodejs)
- dotenv (Loads up env variables)
- express (Web framework of choice)
- @types/expres (Types for express)
Now we can start coding. Create an index.ts
in a src
folder and copy the following code.
import express from "express";
let app = express();
let port = 3000;
app.get("/", (_, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
And to start the application add the following scripts to package.json
file and it will look something like
{
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^17.0.13",
"nodemon": "^2.0.15",
"ts-node": "^10.4.0",
"tsconfig.json": "^1.0.10",
"typescript": "^4.5.5"
},
"dependencies": { "dotenv": "^14.3.2", "express": "^4.17.2" },
"scripts": { "start": "nodemon -r dotenv/config --exec ts-node src/index.ts" }
}
Now in terminal hit yarn start and you’ll have a base express server up and running. You can see the working code in git history.
Step 2: Setting up a GraphQL Server
First add all of the following packages
yarn add apollo-server
yarn add graphql@15.3.0yarn add -D @types/graphql
yarn add -D type-graphqlyarn add reflect-metadata
- apollo-server (Apollo Server — helps you create a production ready graphql server with all the bells and whistles)
- graphql (The official graphql package)
- type-graphql (Helps you create graphql resolvers the typescript way)
- reflect-metadata (metadata reflection api — simply put; used for annotations)
Let’s start coding, first of all put this as the first line on index.ts
import reflect-metadata;
This enables reflection throughout your code and now we can use annotations to enhance our graphql experience. Create a file named graphQLServer.ts
and place the following code in it. Explanation follows
import { Express } from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { DemoResolver } from "./resolvers/demoResolver";
export async function createApolloServer(app: Express) {
let apolloServer = new ApolloServer({ schema: await buildSchema({ resolvers: [DemoResolver] }) });
await apolloServer.start();
apolloServer.applyMiddleware({ app });
return apolloServer;
}
The createApolloServer
initiates an apollo server and takes input the express instance to host its server on. When creating a new ApolloServer
it takes a buildSchema
as an input.
GraphQL works through resolvers. Resolvers work as entry points to all the queries and mutations in an API much like GET or REST in a REST API. For the moment have an empty file named demoResolver and a class named DemoResolver
for simplicity. Also as you can see resolvers is an array and can host as many resolvers as you want.
Next outside the constructor is apolloServer.start()
and the we apply the express middleware that we passed on.
Now in the demoResolver.ts
you can paste the following code.
import { Query, Resolver } from "type-graphql";
@Resolver()
export class DemoResolver {
@Query(() => String) async hello() {
return "hi!";
}
}
Here the type-graphql
plays a crucial role in making your graphql server maintainable. The @Resolver()
makes the class available to the apollo server as a resolver which hosts Queries or Mutations. The function hello()
is basic enough to understand. What really matters here is the @Query(()=>String)
which informs the type-graphql that this query returns a string
.
Lastly, call the createApolloServer
method in index.ts
file and thats it. You can see all the working code in the git history.
You can now do yarn start
and get the server up and running. Head over to your browser and hit <a href="http://localhost:3000/graphql">http://localhost:3000/graphql</a>
and you’ll see Apollo Server Studio where you can see all your queries and mutations listed there.
Working with apollo studio is pretty easy and intuitive. Simply select the query you want to execute and hit Run Query. You’ll see the result
Step 3: Enhance your Resolvers
Now let’s create a Coffee Resolver that returns coffee types. To list down the things we want our resolver to provide.
- Returns a single coffee type by id
- Returns list of coffees
- Updates/Inserts a Coffee object.
Let’s start by returning a coffee object by id. We’ll upgrade our resolver as we go along the way. Firstly let’s create a basic coffee model to work with.
import axios from "axios";
/** * Coffee information from random-data-api.com */ interface CoffeeAPI {
id: string;
blend_name: string;
origin: string;
variety: string;
}
export class Coffee {
id: number;
blendName: string;
origin: string;
variety: string;
/**
* Randomly generates coffee object
* @param id id to search for
* @returns gets coffee object randomly then injects the same id. */
static async getById(id: number) {
let coffee = new Coffee();
try {
let res = await axios.get("https://random-data-api.com/api/coffee/random_coffee");
let json: CoffeeAPI = (await res.data) as CoffeeAPI;
coffee.id = id;
coffee.blendName = json.blend_name;
coffee.origin = json.origin;
coffee.variety = json.variety;
} catch (error) {
console.error(error);
throw error;
} finally {
return coffee;
}
}
static async getList() {
let coffees: Coffee[] = [];
try {
let res = await axios.get("https://random-data-api.com/api/coffee/random_coffee?size=10");
let json: CoffeeAPI[] = (await res.data) as CoffeeAPI[];
json.forEach((item) => {
let coffee = new Coffee();
coffee.id = parseInt(item.id);
coffee.blendName = item.blend_name;
coffee.origin = item.origin;
coffee.variety = item.variety;
coffees.push(coffee);
});
} catch (error) {
console.error(error);
throw error;
} finally {
return coffees;
}
}
static async updateCoffee(coffee: Coffee) {
console.log(coffee);
return true;
}
}
The code is simple. We expose three functions getById(id), getList(), updateCoffee(coffee)
which are pretty self explainatory. Now lets make this model available in our graphQL server. Ideally we want our resolver to return the whole Coffee object and also take it as an input. For that all we need to do is add in;
import { Field, InputType, ObjectType } from "type-graphql";
@InputType("CoffeeInput")
@ObjectType()
export class Coffee {
@Field()
id: number;
@Field()
blendName: string;
@Field()
origin: string;
@Field()
variety: string;
...
}
Describing the annotations
- ObjectType defines that this class will be used as a return object.
- InputType with “CoffeeInput” defines this class will be used as an input. We added a
CoffeeInput
name when defining the input type is because graphql has a strong opinion that objects for input and output needs to be separate. So instead of duplicating the code all we did was change the name. - Field exposes the class members to the graphql object. If a member is missing that annotation, it’s not exposed. Field also have some usefull properties like
nullable
to define if the field can be null anddescription
to add useful comments to the api.
Now lets add a CoffeeResolver.ts
.
import { Coffee } from "../model/coffee";
import { Arg, Mutation, Query, Resolver } from "type-graphql";
@Resolver()
export class CoffeeResolver {
@Query(() => Coffee) async getCoffee(@Arg("id") id: number) {
return await Coffee.getById(id);
}
@Query(() => [Coffee]) async getListOfCoffee() {
return await Coffee.getList();
}
@Mutation(() => Boolean) async updateCoffee(@Arg("coffee", () => Coffee) coffee: Coffee) {
console.log(ctx.user);
return await Coffee.updateCoffee(coffee);
}
}
Here everything is similar to what we did before when defining a DemoResolver. The only difference here are we added a @Mutation()
annotation and have a [Coffee]
return type for getListOfCoffee()
that defines this query will be returning an array of Coffee. You can now start your server and explore all the graphQL exposed apis on the apollo studio.
Step 4: Adding context
What if we wanted to read the headers passed in the http request or you want to load and read the user every time before your code starts making db calls, for this specific reason apollo server provides a feature called Context. It builds up whenever a graphql call is made.
The code is simple, let’s create a context that loads up a dummy user every time a gql call is made so we can make decisions based on the use who made the call. First up is the User class.
import axios from "axios";
import { Field, ObjectType, registerEnumType } from "type-graphql";
export enum Role {
ADMIN = "ADMIN",
VIEWER = "VIEWER",
}
@ObjectType()
export class User {
@Field() id: string;
@Field() name: string;
@Field(() => Role) role: Role;
static async createDummyUser() {
let user = new User();
try {
let res = (await axios.get("https://random-data-api.com/api/users/random_user")).data as {
first_name: string;
id: string;
};
user.id = res.id;
user.name = res.first_name;
} finally {
return user;
}
}
}
registerEnumType(Role, { name: "Role", description: "Available roles for users" });
There are two things to note here, first even though i did not need to define @Field()
annotations but i did so anyway assuming at some point in future we might want to send the user information any way. So no harm in doing that. More importantly, i wanted to go through how we can also use enums in GraphQL. There are two steps to it. One is to obviously define an enum and the other is to register it specifically by
registerEnumType(Role, { name: "Role", description: "Available roles for users" });
This registers the enum globally and can be seen as simple strings but accepts only these values. Now lets have a simple context interface
import { User } from "../model/user";
import { Request, Response } from "express";
/*** Request and response is passed as is if needed.*/ export interface Context {
req: Request;
res: Response;
user: User | null;
}
Lastly, we need to fill this context up whenever we have a graphql call and this needs to be provided when we’re creating the apollo server in our graphQLServer.ts
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { DemoResolver } from "./resolvers/demoResolver";
import { User } from "./model/user";
import { Context } from "./util/context";
export async function createApolloServer(app: Express) {
let apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [DemoResolver, CoffeeResolver],
}),
context: async ({ req, res }): Promise<Context> => {
return {
user: await User.createDummyUser(),
req: req,
res: res,
};
},
});
await apolloServer.start();
apolloServer.applyMiddleware({ app });
}
Only the context
part is really important here. So what’s really left is to access the context in our every Query/Mutation.
@Mutation(() => Boolean)
async updateCoffee(@Ctx() ctx: Context, @Arg("coffee", () => Coffee) coffee: Coffee) {
console.log(ctx.user);
return await Coffee.updateCoffee(coffee);
}
By just adding @Ctx() ctx
we can access the context object everytime. This change-set can be found in the git history.
Step 5: Authorisation
One of the most important item or rather the necessary item to do is Authorisation & having all of that setup its really easy to do so.
Add authChecker: authChecker
when creating your Apollo Server
let apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [DemoResolver, CoffeeResolver],
authChecker: authChecker,
}),
context: async ({ req, res }): Promise<Context> => {
return { user: await User.createDummyUser(), req: req, res: res };
},
});
where the authChecker is
const authChecker: AuthChecker<Context> = (authContext, roles) => {
let user = authContext.context.user;
if (roles.length === 0) {
// if `@Authorized()`, check only if user exists
return user !== null || user !== undefined;
}
if (!user) {
// and if no user, restrict access
return false;
}
if (roles.includes(user.role)) {
// grant access if the roles overlap
return true;
}
// no roles matched, restrict access
return false;
};
The apollo Client exposes the roles as strings assigned to a method and your authChecker gets them in roles in every request. The authContext contains your context so you get the user loaded from the db every time for you to verify if it has access to those mutations. What’s left is to put authorisations on your Queries & Mutations. Fortunately this is very easy to keep track of and is done with the use of annotations @Authorised()
.
@Authorized() @Query(() => [Coffee]) async getListOfCoffee() {
return await Coffee.getList();
}
@Authorized<Role>(Role.ADMIN) @Mutation(() => Boolean) async updateCoffee(
@Ctx() ctx: Context,
@Arg("coffee", () => Coffee) coffee: Coffee
) {
console.log(ctx.user);
return await Coffee.updateCoffee(coffee);
}
The methods annotated with just the @Authorised
tags will just need to be authenticated whatever the roles are. If you want to be more sepecific on what type of role you want the method to be allowed to, provide @Authorized<Role>(Role.ADMIN)
, you can also queue up multiple roles.
You can see the change-set specific to authorisations in the git history.
Conclusion
Hopefully by the end of this tutorial, you’ll have a working GraphQL server which can easily be extended to cater for more needs.
Member discussion