Build a scalable GraphQL server using TypeScript and Apollo Server

There are a lot of tutorials to create Apollo Server and adding typing to it. However, creating a better project structure requires in-depth knowledge of Node.js and Apollo Server. Here in this article, I will explain the step-by-step process to build a robust and scalable server.

Prerequisite:

Before going further, I assume you have basic knowledge of Node.js and TypeScript. If you are new to Node.js, Please have a look at some basic tutorials on Node.js.

1. Initialize a blank project using TypeScript CLI

First, We need to create a project and init the configuration.

## Create a folder
mkdir apollo-server
cd apollo-server
## Init npm package
npm init --y

To build a server, we do need some other libraries. But before building the server, let's create a development environment by adding typescript and its configurations.

## Install required modules
npm install --save-dev ts-node typescript
# (ts-node) will be use to run typescript code directly## Init typescript config
npx tsc --init

Once you run tsc — init, It will create a tsconfig.json. It will have some basic config. Let’s update the config as mentioned below.

// apollo-server/tsconfig.json{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./lib",
"rootDir": "./src",
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

To tryout typescript, We have to add some basic commands in package.json. Update the scripts and main section in package.json.

  "version": "1.0.0",
"description": "",
"main": "lib/index.js",
"scripts": {
"build": "tsc",
"start": "node lib/index.js",
"start:ts": "ts-node src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "xdeepakv",

Here, main is to describe the primary entry point to the node module. Let’s create our first typescript code.

Create an src folder and add index.ts

// apollo-server/src/index.tsconst username = "Deepak"console.log(`Hello ${username}`)

You can use npm build command to convert typescript to javascript. So you can either build(generate) javascript from typescript and run. Or, you can directly run typescript using ts-node.

Build and Run:

npm run build
npm run start
## Output: Hello Deepak

Run with ts-node

npm run start:ts## Output: Hello Deepak

Note: ts-node is not recommended for a production run. Please compile/build and run.

Now your typescript project is ready to fly. Let’s create an Apollo Server.

2. Creating a custom Apollo Server

To create an Apollo Server, We need to install apollo-server and graphql.

npm install apollo-server graphql

Once install, We need to create ApolloServer Instance. Let’s update index.ts

// apollo-server/src/index.tsimport { ApolloServer } from "apollo-server";// Take custom port from ENV
const PORT = process.env.PORT || 4000;
// Instance of ApolloServer
const server = new ApolloServer({
typeDefs: ``,
resolvers: {},
});
server.listen(PORT).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

When you try to run the above code, You will see an error like Error: Apollo Server requires either an existing schema, modules or typeDefs. This is expected since we have not defined any schema and resolver. Every graphql server required schema definition. The schema consists of Queries and mutation definitions. Let’s define a very basic schema and its resolver.

Define User Data type:

const typeDefs = `
type User {
firstname: String
lastname: String
age: Int
address: String
}
type Query {
users:[User]
}
`;

Here above we have defined a basic User data type. The Query is a special type. Every entry in Query considered as an endpoint. This is what we call from the Client.

Now for users query we need to add resolver.

const resolvers = {
Query: {
users: () => [],
},
};

Now let’s update ApolloServer configuration.

import { ApolloServer } from "apollo-server";
const PORT = process.env.PORT || 4000;
// rest of the codeconst server = new ApolloServer({
typeDefs,
resolvers,
});
server.listen(PORT).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

Now when you start the server, you will see the following playground in http://localhost:4000/

Note: If you want to add custom routing in server apart from graphql endpoint. There are different plugin/module to support different Node.js server.

Create an Express Apollo Server: To create a custom express apollo server, We need to install apollo-server-express. After that modify below highlighted changes.

import { ApolloServer } from "apollo-server-express";
import express from "express";
const app = express();

// Rest of the code
const server = new ApolloServer({
typeDefs,
resolvers,
});
server.applyMiddleware({ app });app.get("health", (_, res) => res.send("OK"));app.listen(PORT, () => {
console.log(`🚀 Server ready at
http://localhost:4000${server.graphqlPath}`);
});

In the above code, You can see an express app has been created and apply as middleware to ApolloServer. Now instead of starting ApolloServer, We need to start express app and listen to the defined port.

Note: Once you use apollo-server-express module, The default endpoint will change to http://localhost:4000/graphql. You can see URL endpoint in the logs.

3. Project modularisation

We already have our ApolloServer running. However to build a scalable application. We have to modularise the typedefs, resolvers and custom routes.

Modularise typedefs: Let’s add typeDefs and add three files.

mkdir src/typedefs
touch src/typeDefs/default.ts src/typeDefs/index.ts src/typeDefs/users.ts

Let’s update individual files

// src/typeDefs/default.tsconst { gql } = require("apollo-server-express");export default gql`
type Query {
_: Boolean
}
type Mutation {
_: Boolean
}
`;
// src/typeDefs/users.tsimport { gql } from "apollo-server-express";const typeDefs = gql`
type User {
firstname: String
lastname: String
age: Int
address: String
}
extend type Query {
users: [User]
}
`;
export default typeDefs;
// src/typeDefs/index.tsimport users from "./users";
import defaultSchema from "./default";
export default [defaultSchema, users];

Here default.ts schema has default query and mutation. Now we can extend another schema like users.ts using extend keyword.

We do need to modify the server code to import typeDefs module.

// src/index.tsimport { ApolloServer } from "apollo-server-express";
import express from "express";
import typeDefs from "./typeDefs";// Rest of the codeconst server = new ApolloServer({
typeDefs,
resolvers,
});
// Rest of the codeapp.listen(PORT, () => {
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
});

Similarly, we can extract resolver to another module.

## create resolvers folder and some couple of filesmkdir src/resolvers
touch src/resolvers/index.ts src/resolvers/users.ts

Update the resolvers/index.ts and resolvers/users.ts files.

// src/resolvers/index.tsimport users from "./users";export default [users];// src/resolvers/users.tsconst resolvers = {
Query: {
users: () => [
{
firstname: "deepak",
lastname: "vishwakarma",
},
{
firstname: "deepak",
lastname: "sharma",
},
],
},
};
export default resolvers;

Updated server code

import { ApolloServer } from "apollo-server-express";
import express from "express";
import typeDefs from "./typeDefs";
import resolvers from "./resolvers";
// Rest of the codeconst server = new ApolloServer({
typeDefs,
resolvers,
});
// Rest of the code

Once run the server again, we can try to query it.

curl --location --request POST 'http://localhost:4000/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"{\n users {\n firstname\n }\n}"}'
## Output:{"data":{"users":[{"firstname":"deepak"},{"firstname":"deepak"}]}}

4. Working with data models(database)

Previously we work with static data set in users resolvers. For any server-side application, it should able to connect to the database. But this tutorial is not focused on teaching everything. So we will skip connecting the real database. Instead of that, We will work with in-memory database like sqlite.

Setup SQLite: SQLite is a lightweight database and it has a simple data type. To setup SQLite in Node.js. We do need to install the package. npm install sqlite3 &&npm install @types/sqlite3 -D. Once we install node module, We need to create models directory and create a basic database helper module.

mkdir models
touch src/models/database.ts src/models/index.ts src/models/User.ts

Once you create database.ts file, Let’s add basic boilerplate code to work with SQLite.

// src/models/database.tsimport { promisify } from "util";
import { RunResult, verbose } from "sqlite3";
const sqlite3 = verbose();
const db = new sqlite3.Database(":memory:");
// Some hacks to convert callback functions to promiseconst all = promisify(db.all).bind(db);const run = async (query: string, args: any[]): Promise<{id?: number}> => {
return new Promise((res, rej) => {
db.run(query, args, function (this: RunResult, err: Error) {
if (err) rej(err);
else res({ id: this.lastID });
});
});
};
// on init create database table usersexport const init = () => {
db.serialize(() => {
db.run(`CREATE TABLE users (
firstname TEXT NOT NULL,
lastname TEXT,
age INTEGER NOT NULL,
address TEXT
)`);
});
};
export { all, run };
export default db;

Once we have database.ts ready, We can create User model to query users database table.

// src/models/User.tsimport { all, run } from "./database";interface User {
firstname: string;
lastname: string;
age: number;
address?: string;
}
const TABLE_NAME = "users";
const createUser = async (user: User) => {
return await run(
`INSERT INTO ${TABLE_NAME} (firstname,lastname,age) VALUES(?,?,?)`,
[user.firstname, user.lastname, user.age]
);
};
const getUsers = async () => {
return await all(
`SELECT rowid as id,firstname,lastname,age FROM ${TABLE_NAME}`
);
};
export default { createUser , getUsers };

Here we have created two basic methods to insert a record in the users table and fetch all users from the table. Now, let’s export the models from index.ts.

// src/models/index.tsimport User from "./User";
export interface Models {
models: {
User: typeof User;
};
}
export default { User };

5. Uses Models in Resolvers

Once we created models, We need to update users resolver file to use database model. So that we can work with the models. But how we should do that? This is of the main challenge, to share models to resolvers. However, ApolloServer makes it easy. You can pass context(common api/data) to resolvers. For that, we need to update the ApolloServer config.

import typeDefs from "./typeDefs";
import resolvers from "./resolvers";
// Import database and init database instance.
import { init } from "./models/database";
init();
// Import models
import models from "./models/";
const app = express();const PORT = process.env.PORT || 4000;// Pass the context
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
models,
}),

});
// Rest of the code.

Once you pass models in context, we can easily get this in resolver function in the third arguments. Let’s update resolvers.

// src/resolvers/users.tsimport { Models } from "../models/";const resolvers = {
Query: {
users: async (_: any, __: any, { models }: Models) => {
return await models.User.getUsers();
},
}
};
export default resolvers;

6. Adding User creation Mutation

Now our model is ready to fetch users. However, We need to create a new user in the database table. In order to create a new user, We need to add mutation. The mutation concept is simple. Whenever you need to create or update data, You can think of creating mutation. Let’s update out typeDefs file.

// src/typeDefs/users.tsimport { gql } from "apollo-server-express";const typeDefs = gql`
type User {
id: String
firstname: String
lastname: String
age: Int
address: String
}
input UserInput {
firstname: String
lastname: String
age: Int
address: String
}

extend type Query {
users: [User]
}
extend type Mutation{
createUser(input: UserInput!): User
}

`;
export default typeDefs;

Here in the above example, We have created a mutation createUser which accept UserInput as the argument. Let’s update resolver to support mutation. For that just like Query, we have to add Mutation object in resolver.

// src/resolvers/users.tsconst resolvers = {
Query: {
// rest of the code
},
Mutation: {
createUser: async (_: any, { input: user }: any, { models }: Models) => {
const { id } = await models.User.createUser(user);
return { ...user, id };
},
},

};
export default resolvers;

To create a new record, you can create a mutation in Apollo Client. You can also use postman to query GraphQL server too.

Now your ApolloServer is ready. Enjoy your new typescript Graphql server. There are still a lot of improvements can be done. Such as

  1. Adding module for custom routes/middlewares
  2. Adding support for auth-token in GraphQL
  3. Add guards in GraphQL APIs
  4. Enable caching
  5. Integrate with custom plugins

I will leave above improvements up to you or I will create a new story for that.

Cheers!!🍻🍻 Keep Coding! You can find source code to GitHub repo graph-ql-typescript-starter

I am UI/UX lead developer, tech enthusiastic person. I am working in one of the biggest FinTech company, trying to solve basic challenges on ground issues.