DEV Community

Cover image for How to Build a GraphQL API in Node.js
Francisco Mendes
Francisco Mendes

Posted on • Edited on

How to Build a GraphQL API in Node.js

GraphQL is one of the most flexible and amazing tools we can learn to implement, however the amount of configuration we have to do or the number of tools we have to use to create an API far exceeds the creation of a REST API (this is just my opinion). Obviously with time and practice, it all ends up being a natural process, but the learning curve is simply higher.

That's why I decided to create a series of articles that exemplify the creation of a GraphQL API from scratch, from creating a simple server, to implementing authorizations.

people talking too much

For whom is this series?

I don't think you need to have much experience with creating APIs in GraphQL, but I hope you already have some prior knowledge about some concepts such as:

  • Queries and Mutations
  • Types and Resolvers

Let's configure Node.js

This project will have a very minimalistic configuration and I believe that they are things that they are more than used to.

# NPM
npm init -y

# YARN
yarn init -y

# PNPM
pnpm init -y
Enter fullscreen mode Exit fullscreen mode

Then we go to our package.json to define that the type is module, in order to use ESM in our project. As well as we will install nodemon and we will create the script that will be used during the development of our api.

# NPM
npm install nodemon -D

# YARN
yarn add nodemon -D

# PNPM
pnpm add nodemon -D
Enter fullscreen mode Exit fullscreen mode
{
  //...
  "type": "module",
  "scripts": {
    "dev": "nodemon src/main.js"
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

With this simple setup we can go to the next point.

thats easy

Required Libraries

For the development of our GraphQL API we will install the following dependencies:

  • fastify - this will be our http server
  • apollo-server-fastify - this is the wrapper we are going to use so we can have fastify as our http server
  • apollo-server-core - this dependency holds the main features of apollo server
  • @graphql-tools/load - this will be responsible for loading our *.gql files (file system)
  • @graphql-tools/graphql-file-loader - this one loads the type definitions from graphql documents
  • graphql - the graphql implementation for javascript
  • @graphql-tools/schema - creates a schema from the provided type definitions and resolvers

All the libraries mentioned above are the ones we will need to install to create our project, however we will still have to install others so that we can integrate our project with a database, in this series of articles I will use Sequelize ORM with SQLite database.

  • sequelize - ORM
  • sqlite3 - database

With this list of dependencies in mind, we can proceed with their installation:

# NPM
npm install fastify apollo-server-fastify apollo-server-core @graphql-tools/load @graphql-tools/graphql-file-loader graphql @graphql-tools/schema sequelize sqlite3

# YARN
yarn add fastify apollo-server-fastify apollo-server-core @graphql-tools/load @graphql-tools/graphql-file-loader graphql @graphql-tools/schema sequelize sqlite3

# PNPM
pnpm add fastify apollo-server-fastify apollo-server-core @graphql-tools/load @graphql-tools/graphql-file-loader graphql @graphql-tools/schema sequelize sqlite3
Enter fullscreen mode Exit fullscreen mode

Database Models

Now with everything installed we can proceed to define our database models, in this article we will create just one and this one is similar to other articles in the past. But first let's create our database connection.

// @/src/db/index.js
import Sequelize from "sequelize";

export const databaseConnection = new Sequelize({
  dialect: "sqlite",
  storage: "src/db/dev.db",
  logging: false,
});
Enter fullscreen mode Exit fullscreen mode

Now let's create our model:

// @/src/db/models/Dog.js
import Sequelize from "sequelize";

import { databaseConnection } from "../index.js";

export const DogModel = databaseConnection.define("Dog", {
  id: {
    type: Sequelize.INTEGER,
    primaryKey: true,
    autoIncrement: true,
    allowNull: false,
  },
  name: {
    type: Sequelize.STRING,
    allowNull: false,
  },
  breed: {
    type: Sequelize.STRING,
    allowNull: false,
  },
  isGoodBoy: {
    type: Sequelize.BOOLEAN,
    default: true,
  },
});
Enter fullscreen mode Exit fullscreen mode

And also the entry point of our models:

// @/src/db/models/index.js
export * from "./Dog.js";
Enter fullscreen mode Exit fullscreen mode

With our model created we can move on to the configuration of our Apollo Server.

Configure Apollo Server

It is in the creation of our Apollo Server instance that we will add our schema, we will define our context, as well as middleware and plugins. In this case we will only define the things that are necessary and later we will only have to pass the necessary fields as arguments.

// @/src/apollo/createApolloServer.js
import { ApolloServer } from "apollo-server-fastify";
import { ApolloServerPluginDrainHttpServer } from "apollo-server-core";

export const createApolloServer = ({ app, schema }) => {
  return new ApolloServer({
    schema,
    context: ({ request, reply }) => ({
      request,
      reply,
    }),
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer: app.server }),
      {
        serverWillStart: async () => {
          return {
            drainServer: async () => {
              await app.close();
            },
          };
        },
      },
    ],
  });
};
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, in the function we created we only have one argument to which we destructure and we are going to get two properties, our schema and the app, this app will be our http server instance.

In addition to this, we also added two properties to our context, the request and the reply. If our resolvers need to work with the Fastify request or even with the reply, it can be easily accessible.

lets do it

Types and Resolvers

I bet that many already expected that the next step would be the configuration of our http server, to be different and I think simpler to understand, let's first define and configure our TypeDefs and our resolvers.

Starting first with our type definitions, let's divide them into folders so that we can differentiate between them (Mutations and Queries). As well as we will create a graphql file for each of them.

First, let's create our mutations:

# @/src/graphql/typeDefs/Mutations/AddDog.gql
input addDogInput {
    name: String!
    age: Int!
    breed: String!
    isGoodBoy: Boolean
}

type Mutation {
    addDog(input: addDogInput): Dog
}

# @/src/graphql/typeDefs/Mutations/DeleteDog.gql
type Mutation {
    deleteDog(id: ID!): Dog
}

# @/src/graphql/typeDefs/Mutations/UpdateDog.gql
input updateDogInput {
    name: String
    age: Int
    breed: String
    isGoodBoy: Boolean
    id: ID!
}

type Mutation {
    updateDog(input: updateDogInput!): Dog
}
Enter fullscreen mode Exit fullscreen mode

Now let's create our queries:

# @/src/graphql/typeDefs/Queries/GetDog.gql
type Query {
    getDog(id: ID!): Dog
}

# @/src/graphql/typeDefs/Queries/GetDogs.gql
type Dog {
    id: ID!
    name: String
    age: Int
    breed: String
    isGoodBoy: Boolean
}

type Query {
    getDogs: [Dog]
}
Enter fullscreen mode Exit fullscreen mode

Now we can create our entry point that will be responsible for loading the graphql files and "merging" them.

// @/src/graphql/typeDefs/index.js
import { loadSchemaSync } from "@graphql-tools/load";
import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader";

export const typeDefs = loadSchemaSync("./**/*.gql", {
  loaders: [new GraphQLFileLoader()],
});
Enter fullscreen mode Exit fullscreen mode

We already have our type definitions, as well as their entry point, now we have to work on our resolvers. There are several ways to do this, but I like to go with the simplest one, which is vanilla. What I mean by vanilla is creating each of our resolvers as functions and then assigning each of them to a single entry point, where we then assign each of them to their respective type (Mutation or Query).

First let's work on the resolvers of our mutations:

// @/src/graphql/resolvers/Mutations/addDog.js
import { DogModel } from "../../../db/models/index.js";

export const addDog = async (parent, args, context) => {
  const result = await DogModel.create({ ...args.input });
  return result;
};

// @/src/graphql/resolvers/Mutations/deleteDog.js
import { DogModel } from "../../../db/models/index.js";

export const deleteDog = async (parent, args, context) => {
  const result = await DogModel.findByPk(args.id);
  await DogModel.destroy({ where: { id: args.id } });
  return result;
};

// @/src/graphql/resolvers/Mutations/updateDog.js
import { DogModel } from "../../../db/models/index.js";

export const updateDog = async (parent, args, context) => {
  const { id, ...rest } = args.input;

  await DogModel.update({ ...rest }, { where: { id } });
  const result = await DogModel.findByPk(id);

  return result;
};
Enter fullscreen mode Exit fullscreen mode

And the respective entry point of our mutations:

// @/src/graphql/resolvers/Mutations/index.js
export * from "./addDog.js";
export * from "./updateDog.js";
export * from "./deleteDog.js";
Enter fullscreen mode Exit fullscreen mode

Now let's work on the resolvers of our queries:

// @/src/graphql/resolvers/Queries/getDog.js
import { DogModel } from "../../../db/models/index.js";

export const getDog = async (parent, args, context) => {
  const result = await DogModel.findByPk(args.id);
  return result;
};

// @/src/graphql/resolvers/Queries/getDogs.js
import { DogModel } from "../../../db/models/index.js";

export const getDogs = async (parent, args, context) => {
  const result = await DogModel.findAll();
  return result;
};
Enter fullscreen mode Exit fullscreen mode

And the respective entry point of our queries:

// @/src/graphql/resolvers/Queries/index.js
export * from "./getDog.js";
export * from "./getDogs.js";
Enter fullscreen mode Exit fullscreen mode

Now let's assign the resolvers to their respective types (Mutations, Queries):

// @/src/graphql/resolvers/index.js
import * as Queries from "./Queries/index.js";
import * as Mutations from "./Mutations/index.js";

export const resolvers = {
  Query: {
    ...Queries,
  },
  Mutation: {
    ...Mutations,
  },
};
Enter fullscreen mode Exit fullscreen mode

We finally have our resolvers and our type definitions, we just need to create the entry point to export both (so that they can be obtained in a single file):

// @/src/graphql/index.js
export * from "./typeDefs/index.js";
export * from "./resolvers/index.js";
Enter fullscreen mode Exit fullscreen mode

Now, we can move on to the next step, which is the configuration of our http server.

almost finished

Create HTTP Server

Now, we have reached one of the most important points, which is to glue each of the pieces (modules) that we have made so far. As you can imagine, now we are going to configure our http server, we are going to import the apollo server configuration, we are going to start the connection with our database, among others.

First let's import our dependencies:

// @/src/server.js
import { makeExecutableSchema } from "@graphql-tools/schema";
import fastify from "fastify";

// ...
Enter fullscreen mode Exit fullscreen mode

Then we will import our modules, such as type definitions, resolvers, etc.

// @/src/server.js
import { makeExecutableSchema } from "@graphql-tools/schema";
import fastify from "fastify";

import { typeDefs, resolvers } from "./graphql/index.js";
import { createApolloServer } from "./apollo/index.js";
import { databaseConnection } from "./db/index.js";

// ...
Enter fullscreen mode Exit fullscreen mode

Now let's create a function responsible for initializing our server and setting up everything.

// @/src/server.js
import { makeExecutableSchema } from "@graphql-tools/schema";
import fastify from "fastify";

import { typeDefs, resolvers } from "./graphql/index.js";
import { createApolloServer } from "./apollo/index.js";
import { databaseConnection } from "./db/index.js";

export const startApolloServer = async () => {
  const app = fastify();

  const schema = makeExecutableSchema({
    typeDefs,
    resolvers,
  });

  const server = createApolloServer({ app, schema });
  await server.start();

  await databaseConnection.sync();

  app.register(server.createHandler());

  await app.listen(4000);
};
Enter fullscreen mode Exit fullscreen mode

Last but not least, we just need create the main file of our api.

// @/src/main.js
import { startApolloServer } from "./server.js";

const boostrap = async () => {
  try {
    await startApolloServer();
    console.log(
      "[Apollo Server]: Up and Running at http://localhost:4000/graphql 🚀"
    );
  } catch (error) {
    console.log("[Apollo Server]: Process exiting ...");
    console.log(`[Apollo Server]: ${error}`);
    process.exit(1);
  }
};

boostrap();
Enter fullscreen mode Exit fullscreen mode

Our api is already finished and clicking on the graphql api endpoint will open a new tab in the browser that will lead to Apollo Studio, from here you can test your queries and mutations. It is worth noting that the sqlite database will be created as soon as you initialize your api.

What comes next?

In the next article I will explain how we can implement a simple authentication and authorization system in our GraphQL API. Of course, we will have users, tokens and we will add middleware.

see you next time gif

Top comments (0)