Setup a GraphQL backend with Node.JS, TypeGraphGL and Apollo Server

1. What is Apollo ?

According to their documentation, Apollo is a platform for building a unified graph, a communication layer that helps you manage the flow of data between your application clients (such as web and native apps) and your back-end services. At the heart of the graph is a query language called GraphQL.

The platform is Divided into 3 main parts.

  • Apollo Clients : it's a set of client libraries that allows you to build awesome frontend apps using React, iOS, Kotlin with Apollo.
  • Apollo Backend :  It's Includes a set of tools developed for building backends using Apollo server, Apollo Federation, Apollo Router.
  • Apollo tools : It's Includes a set of tools to enhance your GraphQl Backends and monitor your schema metrics. i will write another post to focus on how to use Rover CLI and Apollo Studio.

Since this post is not focus on presenting the Apollo platform, i will only focus on using GraphQL Backend with Apollo Server.

2. Apollo Server

Apollo Server is an open-source, spec-compliant GraphQL server that's compatible with any GraphQL client. It's the best way to build a production-ready, self-documenting GraphQL API that can use data from any source. apollo server can be used as :

  • A stand-alone GraphQL server, including in a serverless environment
  • An add-on to your application's existing Node.js middleware
  • A gateway federated graph

In this post, we will use Apollo server version 3

3. TypeGraphQL

TypeGraphQL is a set of useful features, like validation, authorization and dependency injection, which helps develop GraphQL APIs quickly & easily! I will Cover Typegraphql features in another post. Basically, it allows your to build a graphQl schema by defining classes and a bit of decorators. We will see how to do that.

4. Setup 🏄🏻‍♂️

In this post i will use typescript with this open source  node-typescript-starter project for our purposes.

  • Open your favorite terminal and clone the starter template with git clone https://github.com/stemmlerjs/simple-typescript starter.git your_project_name  and cd your_project_name
  • Install node dependencies with npm install if you are using npm and yarn add if you are using YARN as a package manager. In this post we will use yarn so you will need to delete the package-lock.json at the root folder to avoid conflict with yarn 's -lock.json file.
  • Then in the terminal , npm start You should be able to see the output
  • install apollo-server-express (we will be using apollo-server with the express package ) and typegraphql with yarn add graphql@15.5.0 express class-validator reflect-metadata type-graphql apollo-server-express

Once done, we are good to go 👨‍💻

5. Todolist 📃

In this post, we are going to build a backend that expose a GraphQL API showing a list of  Events and related informations. Here are some specs

  • Each event has a name, a place where it will take place, a number of available places, a list of speakers, a date and a rating given by subscribers.
  • Each speacker has a name, a phone number, a mail address,  a list of topics he will cover
  • A speacker can attend several events (Not at the same time)

Thus, the API should be able to :

  • List all events and give details for each of them (speackers, rating, etc)
  • List all speackers
  • Show details about a specific Event or speaker.

To go fast, We are not going to setup a real database but will use LowDB, a tiny in-memory database with typescript support to persist data in a .json file.

To be simple without making a relational database, we will have the following objects:

6. Let's create the server.

create 3 typescript (.ts) files inside the src folder :

  • index.ts : this will be the entry point of our graphql server where we will bootstrap our server
  • types.ts : this file will contain all our typegraphql class that will be serialized and stored  as json objects.
  • resolver.ts : This file will be our resolver where we will define queries

6.1 types.ts 📝

Inside this file, add the following :

import { Field, ObjectType, Int, registerEnumType } from 'type-graphql'

export enum EventStatus {
  PASSED,
  PENDING,
  UPCOMMING
}

registerEnumType(EventStatus, {
  name: 'EventStatus',
  description: 'EventStatus type'
})

@ObjectType({ description: 'Location object' })
export class Location {
  @Field() ID: string
  @Field() city: string
  @Field() country: string
  @Field() postalCode: number
}

@ObjectType({ description: 'Speaker object' })
export class Speaker {
  @Field() ID: string
  @Field() name: string
  @Field() phoneNumber: string
  @Field() email: string
  @Field(_type => [String]) topics: Array<string>
  @Field(_type => [String]) events: Array<string>
}

@ObjectType({ description: 'Subscriber object' })
export class Subscriber {
  @Field() ID: string
  @Field() name: string
  @Field() phoneNumber: string
  @Field() email: string
  @Field(_type => Location) address: Location
}

@ObjectType({ description: 'Event object' })
export class Event {
  @Field() ID: string
  @Field() name: string
  @Field(_type => Location) address: Location
  @Field(_type => EventStatus) status: EventStatus
  @Field(_type => Int) availableplace: number
  @Field(_type => Int) rating: number
  @Field(_type => [Speaker]) speakers: Array<Speaker>
}

6.2 resolver.ts 📝

To get started, we will add a basic query to retrieve empty events array . to do so, add the following inside.

import { Query, Resolver } from 'type-graphql'
import { Event } from './types'

@Resolver()
export default class EventResolver {
  @Query(() => [Event])
  getEvents(): Event[] {
    return []
  }
}

Let's give some explanations.

Typegraphql use decorators for class and field properties to perform some actions.  Everything about typegraphql will be covered in another blog post.  for the previous bloc, we have:

  • @Resolver() : this  decorator make a class that will behave like a controller from classic REST frameworks
  • @Query() : this decorator above a function specify that the function is a Query typegraphql also provide @Mutation() for mutations
  • @Subscription() for subscriptions. A decorator can take an argument where to specify the return type of the function. For getEvents() , the function will return an array of Event

6.3 index.ts  📝

import 'reflect-metadata' // Important
import express from 'express'
import { ApolloServer } from 'apollo-server-express'
import EventResolver from './resolver'
import { buildSchema } from 'type-graphql'

// Set a default port to 4000
const PORT = process.env.PORT || 4000

// Bootstrap the server
const bootstrap = async () => {
  const schema = await buildSchema({ resolvers: [EventResolver] })
  const app = express()
  const server = new ApolloServer({ schema })
  await server.start()
  server.applyMiddleware({ app })
  app.listen(PORT, () => console.log(`🚀 GraphQL server ready at http://localhost:${PORT}`))
}
bootstrap().catch(error => console.log(error))

And at this point, everything is ready to start the server with yarn start:dev inside the terminal. We will have the following output.  

This means our server is ready and will accept request coming from the default port. To be sure our server works well,  open http://localhost:4000/graphql  in the browser, and you will be be able to see the apollo's v3 default landing page.

We're up and running!

Hit the "Query your server" button and you will be redirected to Apollo studio sandbox.

As we now have a single query named getEvents(), It will be listed as in the screen. You can play with the sand box and see how it works.  Since Apollo Studio is a great tools to manage graphql schema, i will cover it in another post.

7. Setup LowDB

As said previously, lowdb is a widely used json database. The library is  a pure ESM package, so cannot be imported with our current project setup which output commonjs. To configure our project to use lowdb, we can update our project configs to output ESM with following.

  • Install lowdb with yarn add lowdb
  • Add following lines inside  tsconfig.json file  in compilerOptions
"module": "ES2020",
"moduleResolution": "node",
  • Add following inside package.json file
  "type": "module",
  • Update ts-node package and install 10.x.x version that have  esm support.  In my case i have  ts-node v10.4.0
  • Update the exec script inside nodemon.json file at the root with node --loader ts-node/esm --experimental-specifier-resolution=node ./src/index.ts
  • Restart the server with yarn start:dev everything should now works fine.

8. Update server

At this step, our server is ready to work with lowDb. Now we will setup our JSON database with some mocked data.

  • Update index.ts file and ad the following code
import 'reflect-metadata' // Important
import express from 'express'
import { Low, JSONFile } from 'lowdb'
import { ApolloServer } from 'apollo-server-express'
import EventResolver from './resolver'
import { buildSchema } from 'type-graphql'
import { AppContext, Event, EventStatus, JsonData, Location, Speaker } from './types'

// Set a default port to 4000
const PORT = process.env.PORT || 4000

const loadData = (): JsonData => {
  const data: JsonData = {}

  // Mock locations
  const l1 = new Location()
  l1.ID = '9f9471dc-87bc-11ec-a8a3-0242ac120002'
  l1.country = 'England'
  l1.city = 'London'
  l1.postalCode = 5208

  const l2 = new Location()
  l2.ID = 'e3098c76-87bd-11ec-a8a3-0242ac120002'
  l2.country = 'France'
  l2.city = 'Paris'
  l2.postalCode = 75000

  const l3 = new Location()
  l3.ID = 'db6c8478-87bd-11ec-a8a3-0242ac120002'
  l3.country = 'US'
  l3.city = 'San Francisco, CA'
  l3.postalCode = 94016

  const l4 = new Location()
  l4.ID = 'e8e94ae6-87bd-11ec-a8a3-0242ac120002'
  l4.country = 'Spain'
  l4.city = 'Barcelona'
  l4.postalCode = 8001

  // Mock speakers
  const s1 = new Speaker()
  s1.ID = 'da554858-87be-11ec-a8a3-0242ac120002'
  s1.name = 'Tom kirke'
  s1.phoneNumber = '+44 2045772856'
  s1.email = 'kirke.tom@gmail.com'
  s1.address = l1
  s1.topics = ['GraphQL', 'Docker', 'Typescript', 'React', 'Jamstack']

  const s2 = new Speaker()
  s2.ID = 'da553d18-87be-11ec-a8a3-0242ac120002'
  s2.name = 'Manu Kholns'
  s2.phoneNumber = '+33 678554412'
  s2.email = 'k.manu06@outlook.com'
  s2.address = l2
  s2.topics = ['Node', 'Docker', 'Python', 'CI/CD', 'REST']

  const s3 = new Speaker()
  s3.ID = 'da553c0a-87be-11ec-a8a3-0242ac120002'
  s3.name = 'Adams Renols'
  s3.phoneNumber = '+14844608120'
  s3.email = 'renols.adams@gmail.com'
  s3.address = l3
  s3.topics = ['Docker', 'Kubernetes', 'G-Cloud', 'AWS', 'Devops']

  const s4 = new Speaker()
  s4.ID = 'da55387c-87be-11ec-a8a3-0242ac120002'
  s4.name = 'Franck Zein'
  s4.phoneNumber = '+33 687445512'
  s4.email = 'franck.z@gmail.com'
  s4.address = l4
  s4.topics = ['React', 'Angular', 'Python', 'AWS']

  //Mock events
  const e1 = new Event()
  e1.ID = '7b9b0eb2-87bc-11ec-a8a3-0242ac120002'
  e1.name = 'GraphQL Submit 2022'
  e1.date = '2022-03-07T03:52:44+01:00'
  e1.status = EventStatus.UPCOMING
  e1.availableplaces = 310
  e1.rating = 4
  e1.address = l1
  e1.speakers = [s1, s2]

  const e2 = new Event()
  e2.ID = '7b9b110a-87bc-11ec-a8a3-0242ac120002'
  e2.name = 'Over React-ed 2021'
  e2.date = '2021-11-12T11:30:44+01:00'
  e1.availableplaces = 270
  e2.status = EventStatus.PASSED
  e2.rating = 4
  e2.address = l2
  e2.speakers = [s1, s2, s3]

  const e3 = new Event()
  e3.ID = '7b9b14ca-87bc-11ec-a8a3-0242ac120002'
  e3.name = 'Devops Next Gen'
  e3.date = '2022-05-12T11:30:44+01:00'
  e1.availableplaces = 412
  e3.status = EventStatus.UPCOMING
  e3.rating = 4
  e3.address = l4
  e3.speakers = [s1, s2, s3]

  //Create DB Data
  data.locations = [l1, l2, l3, l4]
  data.speakers = [s1, s2, s3, s4]
  data.events = [e1, e2, e3]

  return data
}
const setupDb = async (): Promise<Low<JsonData>> => {
  const adapter = new JSONFile<JsonData>('db.json')
  const db = new Low(adapter)
  await db.read()
  db.data = loadData()
  await db.write()
  return db
}

// Bootstrap the server
const bootstrap = async () => {
  const db = await setupDb()
  const schema = await buildSchema({ resolvers: [EventResolver] })
  const app = express()
  const server = new ApolloServer({
    schema,
    context: async ({ req, res }): Promise<AppContext> => {
      return {
        req,
        res,
        db
      }
    }
  })
  await server.start()
  server.applyMiddleware({ app })
  app.listen(PORT, () => console.log(`🚀 GraphQL server ready at http://localhost:${PORT}/graphql`))
}
bootstrap().catch(error => console.log(error))

Let's explain theses updates. we added two functions:

  • loadData() which is a function that mock some demo data for locations, speakers, and events.
  • setupDb() which setup lowDb and load mocked datas into it. Thus, we will be able to query theses data in our resolvers functions

Add two custom types inside type.ts file

export type JsonData = {
  events?: Event[]
  speakers?: Speaker[]
  subscribers?: Subscriber[]
  locations?: Location[]
}

export type AppContext = {
  req: Request
  res: Response
  db: Low<JsonData>
}
  • Inside resolver.ts add resolver functions to get events and speackers
import { Arg, Ctx, Query, Resolver } from 'type-graphql'
import { AppContext, Event, Speaker } from './types'

@Resolver()
export default class EventResolver {
  @Query(() => [Event])
  getEvents(@Ctx() { db }: AppContext): Event[] | undefined {
    return db.data?.events
  }

  @Query(() => Event)
  getEvent(@Arg('id', () => String) id: string, @Ctx() { db }: AppContext): Event | undefined {
    return db.data?.events?.find((event: Event) => event.ID === id)
  }

  @Query(() => [Speaker])
  getSpeakers(@Ctx() { db }: AppContext): Speaker[] | undefined {
    return db.data?.speakers
  }

  @Query(() => Event)
  getSpeaker(@Arg('id', () => String) id: string, @Ctx() { db }: AppContext): Speaker | undefined {
    return db.data?.speakers?.find((speaker: Speaker) => speaker.ID === id)
  }
}

And here, everything is ready. But before querying our server, let’s  explain what one of theses functions does. getEvents() function for example.

@Query(() => [Event])
  getEvents(@Ctx() { db }: AppContext): Event[] | undefined {
    return db.data?.events
  }

There is not a lot to say here, we just passed the db instance through the context and got it using @Ctx() decorator. And next, we just return all events.  

9. let's test it all.

Open apollo studio sandbox at localhost:4000/graphql and query the server.

and here we have the result on this screen.

  • section 1 : this bloc display all queries, mutations, subscriptions you write inside your resolver class with description about each functions, fields and types
  • section 2: this section is where you write your request query, mutation or subscription
  • section 3: this section will just display the result.

Thanks for reading, the source code is available here.

Follow me on socials media to stay up to date with latest posts.