Logo for ammarahmed.ca
BackTech
Setting up a GraphQL API with Node.js cover image
Dec 12, 2022

Setting up a GraphQL API with Node.js

#Backend
#API

As I’ve grown quite fond of GraphQL API’s in the past months, I wanted to take this opportunity to document how to get a GraphQL API up and running in Node.js using TypeScript, TypeGraphQL, Express and Apollo.

What is GraphQL?

GraphQL is query language for API’s which simplifies making requests and makes them more data efficient as compared to the more traditional REST API’s. GraphQL provides static typing for requests and allows users to select exactly which fields they need. It also allows for querying data of different types in the same request which would typically require multiple requests with a REST API. To add on, one of the most convenient features of GraphQL API’s is that all requests are made the same endpoint. To illustrate the benefits of GraphQL API’s over REST API’s, I will provide an example of the same use-case using both GraphQL and REST.

The Example

Let’s say that we are looking to query user data as well as game data for a chess game on the same page. However, we only need the user’s first name and last name and only the current status of the game.

REST API

For the user data, we’d make a GET request with a query parameter for the username. For example, http://localhost:8080/user?username=ammar123

This would return a JSON object containing the entire user object:

1{ 2 "data": { 3 "username": "ammar123", 4 "email":"ammar123@email.com", 5 "phoneNumber": "1234567890", 6 "firstName": "Ammar", 7 "lastName": "Ahmed", 8 "age": 21 9 } 10} 11

You would then select the first and last name to render in the webpage.

For the game data, we’d make another GET request with a query parameter for the id. For example, https://localhost:8080/game?id=foobar

This would return a JSON object containing the entire game object:

1{ 2 "data": { 3 "id": "foobar", 4 "gameStatus": "active", 5 "colorToMove": "white", 6 "playerIDs": { 7 "white": "bing", 8 "black": "bong" 9 }, 10 "takes": { 11 "white": ["pawn", "pawn", "knight"], 12 "black": ["queen"] 13 } 14 } 15}

You would then select the game status to render in the webpage.

GraphQL API

Using GraphQL, we can make a single request to all the data we need. The request would be a POST request to an endpoint looking something like: https://localhost:8080/graphql with the following in the body:

1query{ 2 getUser(username: "ammar123"){ 3 firstName 4 lastName 5 } 6 7 getGame(id: "foobar"){ 8 gameStatus 9 } 10}

Which would return a JSON object containing the user and game containing only the parameters requested.

1{ 2 "data": { 3 "getUser": { 4 "firstName": "Ammar", 5 "lastName": "Ahmed" 6 }, 7 "getGame": { 8 "gameStatus": "active" 9 } 10 } 11}

As you might imagine, this is extremely useful for requests in which you are requesting a large amount of data as you will only be getting the parameters requested and none others. This is also extremely useful for limiting the number of requests made to a server as multiple queries can be grouped together with ease.

Setting up the GraphQL API

Setup

To start off we should create a folder and initialize a NPM package inside it using the command

1npm init -y # yarn init -y

Next, we want to configure the project to use TypeScript. Create a file called tsconfig.json inside your project directory and add the following:

1{ 2 "compilerOptions": { 3 "target": "es2018", /* TypeGraphQL uses features from ES2018 */ 4 "experimentalDecorators": true, /* Allows using experimental decorators (for TypeGraphQL) */ 5 "emitDecoratorMetadata": true, /* Related to above */ 6 "module": "commonjs", /* Specify what module code is generated. */ 7 "rootDir": "./src", /* Root folder with all source files. */ 8 "outDir": "./dist", /* Output folder for all emitted files. */ 9 "esModuleInterop": true, 10 "forceConsistentCasingInFileNames": true, 11 "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 12 "strictPropertyInitialization": false, /* Allows for creating class properties without initialization */ 13 } 14}

This allows for the TypeScript compiler to compile our code into JavaScript in the way we need. We have also included the options for rootDir and outDir, this means that all our source (TypeScript) files will be housed in the src folder and, when compiled, JavaScript will be emitted to the dist folder.

Next, we need to install all the required packages (NOTE: it’s important to install graphql and type-graphql at the versions displayed below to mitigate errors with Apollo.)

1npm i --save @apollo/server express cors graphql@^16.0.0 class-validator type-graphql@^2.0.0-beta.1 reflect-metadata 2# yarn add @apollo/server express cors graphql@^16.0.0 class-validator type-graphql@^2.0.0-beta.1 reflect-metadata

We also want to install a few dev dependencies to simplify our development process as well as to actually compile the TypeScript:

1npm i --save-dev tsc ts-node nodemon typescript @types/node @types/express @types/cors 2# yarn add -D tsc ts-node nodemon typescript @types/node @types/express @types/cors

Next, inside package.json, we want to add the following to the "scripts" object:

1"scripts": { 2 "start": "node ./dist/index.js", 3 "build": "tsc", 4 "dev": "nodemon --watch './**/*.ts' --exec ts-node ./src/index.ts" 5}

start: Runs the compiled server

build: Compiles code inside ./src to ./dist

dev: Runs code while developing, restarts when .ts files are changed

Code

First, we want to create our main server file. Create a folder called src and create a file called index.ts inside of it.

At the top of the index.ts file, we will import the necessary modules.

1import "reflect-metadata" // Required for TypeGraphQL 2import { ApolloServer } from "@apollo/server"; 3import { expressMiddleware } from "@apollo/server/express4" 4import express from "express"; 5import cors from "cors"; 6import { 7 ObjectType, 8 Resolver, 9 Int, 10 Field, 11 Query, 12 Arg, 13 buildSchema, 14 Mutation 15} from "type-graphql";

For the sake of simplicity, we will keep all our code in the same file, however, it is always better to modularize code and split related classes and functions into separate files.

To start off, let’s create our User and Game classes which will be auto-generated into GraphQL types with TypeGraphQL.

User

Here is what the User class will look like:

1// User Type 2@ObjectType() 3class User{ 4 constructor(params : { 5 username: string, 6 email: string, 7 phoneNumber: string, 8 firstName: string, 9 lastName: string, 10 age: number 11 }){ 12 Object.assign(this, params); 13 } 14 @Field() 15 public username: string; 16 17 @Field() 18 public email: string; 19 20 @Field() 21 public phoneNumber: string; 22 23 @Field() 24 public firstName: string; 25 26 @Field() 27 public lastName: string; 28 29 @Field(type => Int) // must specify this, otherwise, it will be float 30 public age: number; 31}

TypeGraphQL will auto-generate a GraphQL schema type using this class which will look something like:

1type User { 2 username: String! 3 email: String! 4 phoneNumber: String! 5 firstName: String! 6 lastName: String! 7 age: Int! 8}

Game

The Game class implements nested objects, so, we will need to create classes for those as well. All of those classes and the Game class are shown below:

1// Game Type 2@ObjectType() 3class PlayerIDs{ 4 constructor(w: string = "", b: string = "") { 5 this.white = w; 6 this.black = b; 7 } 8 @Field() 9 public white: string; 10 11 @Field() 12 public black: string; 13} 14 15@ObjectType() 16class Takes{ 17 constructor(w: string[] = [], b: string[] = []){ 18 this.white = w; 19 this.black = b; 20 } 21 @Field(type => [String]) 22 public white: string[]; 23 24 @Field(type => [String]) 25 public black: string[]; 26} 27 28@ObjectType() 29class Game{ 30 31 constructor(params : { 32 id: string, 33 gameStatus: string, 34 colorToMove: string, 35 playerIDs: PlayerIDs, 36 takes: Takes 37 }){ 38 Object.assign(this, params); 39 } 40 41 @Field() 42 public id: string; 43 44 @Field() 45 public gameStatus: string; 46 47 @Field() 48 public colorToMove: string; 49 50 @Field(type => PlayerIDs) 51 public playerIDs: PlayerIDs; 52 53 @Field(type => Takes) 54 public takes: Takes; 55}

This will generate the following GraphQL types:

1type PlayerIDs { 2 white: String! 3 black: String! 4} 5 6type Takes { 7 white: [String!]! 8 black: [String!]! 9} 10 11type Game { 12 id: String! 13 gameStatus: String! 14 colorToMove: String! 15 playerIDs: PlayerIDs! 16 takes: Takes! 17}

Next, we will create our resolvers for these classes. Resolvers in GraphQL are essentially the function that is called when a request is made. So, since we are calling getUser and getGame , we need to implement resolvers for both of these. For the sake of simplicity, I will hard code the “database”.

Here is what the resolver would look like:

1@Resolver() 2class MyResolver{ 3 constructor( 4 private database = { 5 users: [{ 6 username: "ammar123", 7 email: "ammar123@email.com", 8 firstName: "Ammar", 9 lastName: "Ahmed", 10 phoneNumber: "1234567890", 11 age: 21 12 }], 13 games: [{ 14 id: "foobar", 15 gameStatus: "active", 16 colorToMove: "w", 17 playerIDs: new PlayerIDs("ammar123", "bingbong"), 18 takes: new Takes(["pawn", "pawn", "knight"], ["queen"]) 19 }] 20 } 21 ){} 22 @Query(returns => User) 23 getUser( 24 @Arg("username") username: string 25 ){ 26 // pull data from database 27 const user = this.database.users.find( user => user.username === username ); 28 29 if (!user) throw new Error("User not found!"); 30 31 return new User(user); 32 } 33 34 @Query(returns => Game) 35 getGame( 36 @Arg("id") id: string 37 ){ 38 // pull data from database 39 const game = this.database.games.find( game => game.id === id import "reflect-metadata" // Required for TypeGraphQL 40 41 if (!game) throw new Error("Game not found!") 42 43 return new Game(game); 44 45 } 46}

This will generate the following in the GraphQL schema:

1type Query { 2 getGame(id: String!): Game! 3 getUser(username: String!): User! 4}

Next, we will create an IIFE (immediately invoked function expression) to run our server with. This is basically a function that calls itself. With this, we wont need to write a specific function to call, whenever we run our index.ts file, our server will be started. Inside of this function, we will generate our GraphQL schema and server and serve it with express.

Here is what this will look like:

1( async () => { 2 3 const schema = await buildSchema({ 4 resolvers: [MyResolver], 5 emitSchemaFile: { 6 path: __dirname + "/schema.gql", // this wil generate a graphql schema file for us to look at 7 } 8 }) 9 10 const app = express(); // the express server 11 12 // the graphql server 13 const server = new ApolloServer({ 14 schema 15 }); 16 17 await server.start(); 18 19 app.use( 20 "/graphql", // the endpoint where our graphql server is hosted 21 cors<cors.CorsRequest>(), // cors middleware 22 express.json(), // parsing request body to json middleware 23 expressMiddleware(server) // middleware for apollo server 24 ) 25 26 const PORT = process.env.PORT || 8080; 27 28 // Starting express server 29 app.listen(PORT, () => console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`)); 30 31})()

And that’s it! Your GraphQL server is ready to go. You can test it out by running the command: npm run dev or yarn dev. This will expose an endpoint at http://localhost:8080/graphql. If you input this URL into a browser, it will open up to playground included with Apollo in which you can make GraphQL requests and debug your API.

It will look something like this:

Apollo GraphQL local testing page
Apollo GraphQL local testing page

Here you can write out GraphQL requests and test them out. It also auto-generates documentation for your API on the left hand side. To pull the same data as mentioned in our above example, we can write out the request and use variables:

1query GetData($username: String!, $gameId: String!){ 2 getUser(username: $username){ 3 firstName 4 lastName 5 } 6 7 getGame(id: $gameId){ 8 gameStatus 9 } 10}

To set the variables, we enter them as JSON in the bottom variables panel. Pressing run, we will see our response on the right hand side as depicted below:

Writing and testing a query in the Apollo GraphQL local testing page
Writing and testing a query in the Apollo GraphQL local testing page

Mutations

What if we wanted to update something in our “database”? With REST APIs, we’d make a POST, DELETE or PUT request, however, with GraphQL, all of these are grouped into one: mutations.

For the sake of our example, let’s say we wanted to update our user’s first or last name. We’d create another function in our resolver with the decorator @Mutation. Here is what this function would look like:

1@Mutation(returns => User) 2 updateName( 3 @Arg("username") username: string, 4 @Arg("firstName", { nullable: true }) firstName?: string, 5 @Arg("lastName", { nullable: true }) lastName?: string 6 ){ 7 const userIdx = this.database.users.findIndex( user => user.username === username) 8 9 if (userIdx === -1) throw new Error("User not found!") 10 11 if (firstName){ 12 this.database.users[userIdx].firstName = firstName; 13 } 14 15 if (lastName){ 16 this.database.users[userIdx].lastName = lastName; 17 } 18 19 return new User(this.database.users[userIdx]) 20 }

So, as you can see, GraphQL is a very simply yet very powerful tool and now you will be able to quickly setup and play around with a GraphQL API yourself.

I have included the entire index.ts file below, you can also find all the code on GitHub.

1import "reflect-metadata" // Required for TypeGraphQL 2import { ApolloServer } from "@apollo/server"; 3import { expressMiddleware } from "@apollo/server/express4" 4import express from "express"; 5import cors from "cors"; 6import { 7 ObjectType, 8 Resolver, 9 Int, 10 Field, 11 Query, 12 Arg, 13 buildSchema, 14 Mutation 15} from "type-graphql"; 16 17// User Type 18@ObjectType() 19class User{ 20 constructor(params : { 21 username: string, 22 email: string, 23 phoneNumber: string, 24 firstName: string, 25 lastName: string, 26 age: number 27 }){ 28 Object.assign(this, params); 29 } 30 @Field() 31 public username: string; 32 33 @Field() 34 public email: string; 35 36 @Field() 37 public phoneNumber: string; 38 39 @Field() 40 public firstName: string; 41 42 @Field() 43 public lastName: string; 44 45 @Field(type => Int) // must specify this, otherwise, it will be float 46 public age: number; 47} 48 49// Game Type 50@ObjectType() 51class PlayerIDs{ 52 constructor(w: string = "", b: string = "") { 53 this.white = w; 54 this.black = b; 55 } 56 @Field() 57 public white: string; 58 59 @Field() 60 public black: string; 61} 62 63@ObjectType() 64class Takes{ 65 constructor(w: string[] = [], b: string[] = []){ 66 this.white = w; 67 this.black = b; 68 } 69 @Field(type => [String]) 70 public white: string[]; 71 72 @Field(type => [String]) 73 public black: string[]; 74} 75 76@ObjectType() 77class Game{ 78 79 constructor(params : { 80 id: string, 81 gameStatus: string, 82 colorToMove: string, 83 playerIDs: PlayerIDs, 84 takes: Takes 85 }){ 86 Object.assign(this, params); 87 } 88 89 @Field() 90 public id: string; 91 92 @Field() 93 public gameStatus: string; 94 95 @Field() 96 public colorToMove: string; 97 98 @Field(type => PlayerIDs) 99 public playerIDs: PlayerIDs; 100 101 @Field(type => Takes) 102 public takes: Takes; 103} 104 105@Resolver() 106class MyResolver{ 107 constructor( 108 private database = { 109 users: [{ 110 username: "ammar123", 111 email: "ammar123@email.com", 112 firstName: "Ammar", 113 lastName: "Ahmed", 114 phoneNumber: "1234567890", 115 age: 21 116 }], 117 games: [{ 118 id: "foobar", 119 gameStatus: "active", 120 colorToMove: "w", 121 playerIDs: new PlayerIDs("ammar123", "bingbong"), 122 takes: new Takes(["pawn", "pawn", "knight"], ["queen"]) 123 }] 124 } 125 ){} 126 @Query(returns => User) 127 getUser( 128 @Arg("username") username: string 129 ){ 130 // pull data from database 131 const user = this.database.users.find( user => user.username === username ); 132 133 if (!user) throw new Error("User not found!"); 134 135 return new User(user); 136 } 137 138 @Query(returns => Game) 139 getGame( 140 @Arg("id") id: string 141 ){ 142 // pull data from database 143 const game = this.database.games.find( game => game.id === id ); 144 145 if (!game) throw new Error("Game not found!") 146 147 return new Game(game); 148 149 } 150 151 @Mutation(returns => User) 152 updateName( 153 @Arg("username") username: string, 154 @Arg("firstName", { nullable: true }) firstName?: string, 155 @Arg("lastName", { nullable: true }) lastName?: string 156 ){ 157 const userIdx = this.database.users.findIndex( user => user.username === username) 158 159 if (userIdx === -1) throw new Error("User not found!") 160 161 if (firstName){ 162 this.database.users[userIdx].firstName = firstName; 163 } 164 165 if (lastName){ 166 this.database.users[userIdx].lastName = lastName; 167 } 168 169 return new User(this.database.users[userIdx]) 170 } 171 172} 173 174( async () => { 175 176 const schema = await buildSchema({ 177 resolvers: [MyResolver], 178 emitSchemaFile: { 179 path: __dirname + "/schema.gql", // this wil generate a graphql schema file for us to look at 180 } 181 }) 182 183 const app = express(); // the express server 184 185 // the graphql server 186 const server = new ApolloServer({ 187 schema 188 }); 189 190 await server.start(); 191 192 app.use( 193 "/graphql", // the endpoint where our graphql server is hosted 194 cors<cors.CorsRequest>(), // cors middleware 195 express.json(), // parsing request body to json middleware 196 expressMiddleware(server) // middleware for apollo server 197 ) 198 199 const PORT = process.env.PORT || 8080; 200 201 // Starting express server 202 app.listen(PORT, () => console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`)); 203 204})()