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
andcd your_project_name
- Install node dependencies with
npm install
if you are usingnpm
andyarn add
if you are usingYARN
as a package manager. In this post we will use yarn so you will need to delete thepackage-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. ForgetEvents()
, the function will return an array ofEvent
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 incompilerOptions
"module": "ES2020",
"moduleResolution": "node",
- Add following inside
package.json
file
"type": "module",
- Update
ts-node
package and install10.x.x
version that have esm support. In my case i havets-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.