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.
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
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
{
//...
"type": "module",
"scripts": {
"dev": "nodemon src/main.js"
},
// ...
}
With this simple setup we can go to the next point.
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
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,
});
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,
},
});
And also the entry point of our models:
// @/src/db/models/index.js
export * from "./Dog.js";
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();
},
};
},
},
],
});
};
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.
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
}
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]
}
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()],
});
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;
};
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";
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;
};
And the respective entry point of our queries:
// @/src/graphql/resolvers/Queries/index.js
export * from "./getDog.js";
export * from "./getDogs.js";
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,
},
};
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";
Now, we can move on to the next step, which is the configuration of our http server.
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";
// ...
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";
// ...
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);
};
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();
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.
Top comments (0)