How To Build a GraphQL API with Node.js, Prisma, Docker and Postgres
Nditah
Posted on August 9, 2021
Introduction
REST (REpresentational State Transfer) is an architectural style for distributed hypermedia systems.
An API is a set of definitions and protocols for building and integrating application software.
Whereas REST is an architectural style, GraphQL is data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data.
GraphQL was developed to solves many of the challeneges of flexibility and efficiency that developers encountered when interacting with RESTful APIs.
GraphQL is not a programming language capable of arbitrary computation, but is instead a language used to make requests to application services that have capabilities defined in this specification.
Prisma is an open-source ORM for Nodejs and TypeScript. In our previous lesson on "How To Build a REST API with Node.js, Prisma ORM, PostgreSQL Database, and Docker" you implemented your first REST API Route. Today, we are going to take a step further to build a Grapghql API with Nodejs, Prisma, and Postgres.
In this lesson, you will use GraphQL and Prisma in combination as their responsibilities complement each other.
Youโll build a GraphQL API for a college management application in JavaScript using Node.js.
You will first use ApolloServer to build the GraphQL API backed by in-memory data structures.
Content
๐ท Step 1 โ Creating the Node.js Project
๐ท Step 2 โ Defining the GraphQL Schema
๐ท Step 2 โ Defining the GraphQL Resolvers
๐ท Step 3 โ Creating the GraphQL Server
๐ท Step 4 โ Setting Up Prisma with PostgreSQL
๐ท Step 5 โ Defining the Data Model with Prisma Migrate
๐ท Step 6 โ Using Prisma Client in the GraphQL Resolvers
๐ท Step 7 โ Creating and Migrating the PostgreSQL Database in App Platform
๐ท Step 8 โ Modifying the Student Model
๐ท Step 9 โ Testing
The Github repository of this project can be found here.
Prerequisites
- Node.js v10 to v14
- Docker installed on your computer (to run the PostgreSQL database locally).
- Basic familiarity with Node.js is helpful, but not strictly required for this lesson.
Step 1 โ Creating the Node.js Project
In this step, you will set up a Node.js project with npm and install the dependencies apollo-server and graphql.
This project will be the foundation for the GraphQL API that youโll build and deploy throughout this tutorial.
First, create a new directory for your project:
$ mkdir node-graphql
Next, navigate into the directory and initialize an empty npm project:
cd node-graphql
npm init --yes
This command creates a minimal package.json file that is used as the configuration file for your npm project.
You will receive the following output:
Output
Wrote to /home/Projects/lesson/node-graphql/package.json:
{
"name": "node-graphql",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Youโre now ready to configure TypeScript in your project.
Execute the following command to install the necessary dependencies:
$ npm install apollo-server graphql
This installs two packages as dependencies in your project:
apollo-server: The HTTP library that you use to define how GraphQL requests are resolved and how to fetch data.
graphql: is the library youโll use to build the GraphQL schema.
Youโve created your project and installed the dependencies. In the next step you will define the GraphQL schema.
Step 2 โ Defining the GraphQL Schema and Resolvers
In this step, you will define the GraphQL schema and corresponding resolvers. The schema will define the operations that the API can handle. The resolvers will define the logic for handling those requests using in-memory data structures, which you will replace with database queries in the next step.
First, create a new directory called src that will contain your source files:
$ mkdir src
Then run the following command to create the file for the schema:
$ touch src/schema.js
Now add the following code to the file:
//* node-graphql/src/schema.js
const { gql } = require('apollo-server')
const typeDefs = gql`
type Student {
id: ID!
email: String!
fullName: String!
dept: String
enrolled: Boolean
}
type Query {
enrollment: [Student!]
students: [Student!]!
student(id: ID!): Student
}
type Mutation {
registerStudent(email: String!, fullName: String!, dept: String): Student!
enroll(id: ID!): Student
}
`
module.exports = {
typeDefs,
}
Here you define the GraphQL schema using the gql tagged template. A schema is a collection of type definitions (hence typeDefs) that together define the shape of queries that can be executed against your API. This will convert the GraphQL schema string into the format that Apollo expects. Node the ! sign on id: ID! for example. It means id of type ID cannot be null. Read more from here.
The schema introduces three types:
Student: Defines the type for a student in your college app and contains four fields where each field is followed by its type, for example, String.
Query: Defines the enrollment query which returns multiple students as denoted by the square brackets and the student query which accepts a single argument and returns a single Student.
Mutation: Defines the registerStudent mutation for creating a registering Student and the enroll mutation which accepts an id and returns a Student.
Note that every GraphQL API has a query type and may or may not have a mutation type. These types are the same as a regular object type, but they are special because they define the entry point of every GraphQL query.
Next, create a file database.js in your project src and add the students array to it as shown below:
// node-graphql/src/database.js
const students = [
{
id: 1,
email: 'ada@telixia.com',
fullName: 'Ada Eze',
dept: 'Software Engineering',
enrolled: true,
},
{
id: 2,
email: 'musa@telixia.com',
fullName: 'Musa Bashir',
dept: 'Data Engineering',
enrolled: true,
},
{
id: 3,
email: 'ola@telixia.com',
fullName: 'Omolara Liza',
dept: 'System Security',
enrolled: false,
},
]
module.exports = {
students,
}
You define the students array with three pre-defined students. Notice that the structure of each student object matches the Student type you defined in the schema. This array holds the students that will be served by the API. In a subsequent step, you will replace the array once the database and Prisma Client are introduced.
Next, create a file resolver.js in your project src and define the resolvers object. Import the students array to it as shown below:
// node-graphql/src/resolvers.js
const { students } = require('./database.js');
const resolvers = {
Student: {
id: (parent, args, context, info) => parent.id,
email: (parent) => parent.email,
fullName: (parent) => parent.fullName,
dept: (parent) => parent.dept,
enrolled: (parent) => parent.enrolled,
},
Query: {
enrollment: (parent, args) => {
return students.filter((student) => student.enrolled)
},
student: (parent, args) => {
return students.find((student) => student.id === Number(args.id))
},
},
Mutation: {
registerStudent: (parent, args) => {
students.push({
id: students.length + 1,
email: args.email,
fullName: args.fullName,
dept: args.dept,
enrolled: false,
})
return students[students.length - 1]
},
enroll: (parent, args) => {
const studentToEnroll = students.find((student) => student.id === Number(args.id))
studentToEnroll.enrolled = true
return studentToEnroll
},
},
}
module.exports = {
resolvers,
}
You define the resolvers following the same structure as the GraphQL schema. Every field in the schemaโs types has a corresponding resolver function whose responsibility is to return the data for that field in your schema. For example, the Query.enrollment() resolver will return the enrolled students by filtering the students array.
Resolver functions receive four arguments namely: parent, args, context, and info. See a brief explanation below:
parent: The parent is the return value of the previous resolver in the resolver chain. For top-level resolvers, the parent is undefined, because no previous resolver is called. For example, when making a enrollment query, the query.enrollment() resolver will be called with parentโs value undefined and then the resolvers of Student will be called where parent is the object returned from the enrollment resolver.
args: This argument carries the parameters for the query, for example, the student query, will receive the id of the student to be fetched.
context: An object that gets passed through the resolver chain that each resolver can write to and read from, which allows the resolvers to share information.
info: An AST representation of the query or mutation. You can read more about the details in part III of this series: Demystifying the info Argument in GraphQL Resolvers.
Since the context and info are not necessary in these resolvers, only parent and args are defined.
Step 3 โ Creating the GraphQL Server
In this step, you will create the GraphQL server with Apollo Server and bind it to a port so that the server can accept connections.
First, run the following command to create the file for the server:
$ touch src/index.js
Now add the following code to the file:
// node-graphql/src/index.js
const { ApolloServer } = require('apollo-server')
const { typeDefs } = require('./schema')
const { resolvers } = require('./resolvers')
const port = process.env.PORT || 9090;
const server = new ApolloServer({ resolvers, typeDefs });
server.listen({ port }, () => console.log(`Server runs at: http://localhost:${port}`));
Here you instantiate the server and pass the imported resolvers and typeDefs from the previous step.
The port the server will bind to is set from the PORT environment variable and if not set, it will default to 9090. The PORT environment variable will be automatically set by App Platform and ensure your server can accept connections once deployed.
Your GraphQL API is ready to run. Start the server with the following command:
$ node src/index.js
You will receive the following output:
Output
Server ready at: http://localhost:9090
Itโs considered good practice to add a start script to your package.json so that the entry point to your server is clear. Moreover, this will allow App Platform to start the server once deployed.
To do so, add the following line to the "scripts" object in package.json:
{
"name": "node-graphql",
"version": "1.0.0",
"description": "Grapghql API with Nodejs, Prisma, Postgres and Docker",
"main": "index.js",
"scripts": {
"start": "node src/"
},
"keywords": ["Grapghql", "API", "Node.js", "Prisma", "Postgres", "Docker"],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server": "^2.18.2",
"graphql": "^15.3.0"
}
}
Now you can start the server with the following command:
$ npm start
To test the GraphQL API, open the URL from the output, which will lead you to the GraphQL Playground.
If you encounter and error like this:
Error: listen EADDRINUSE: address already in use :::9090
at Server.setupListenHandle [as _listen2] (net.js:1320:16)
at listenInCluster (net.js:1368:12)
at Server.listen (net.js:1454:7)
at /home/peace/Projects/Lesson/node-graphql/node_modules/apollo-server/dist/index.js:74:24
at new Promise (<anonymous>)
at ApolloServer.listen (/home/peace/Projects/Lesson/node-graphql/node_modules/apollo-server/dist/index.js:72:15)
Emitted 'error' event on Server instance at:
at emitErrorNT (net.js:1347:8)
at processTicksAndRejections (internal/process/task_queues.js:82:21) {
code: 'EADDRINUSE',
errno: -98,
syscall: 'listen',
address: '::',
port: 9090
}
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! node-graphql@1.0.0 start: `node ./src`
npm ERR! Exit status 1
$ npx kill-port 9090
$ npm start
CLick on Query your server
The GraphQL Playground is an IDE where you can test the API by sending queries and mutations.
For example, to test the enrollment query which only returns enrolled students, enter the following query to the left side of the IDE and send the query by pressing the Run button:
Operation
query ExampleQuery {
enrollment {
id
email
fullName
dept
}
}
Response
{
"data": {
"enrollment": [
{
"id": "1",
"email": "ada@telixia.com",
"fullName": "Ada Eze",
"dept": "Software Engineering"
},
{
"id": "2",
"email": "musa@telixia.com",
"fullName": "Musa Bashir",
"dept": "Data Engineering"
}
]
}
}
To test the registerStudent mutation, enter the following mutation:
Operation
mutation {
registerStudent(
email: "contact@telixia.com",
fullName: "Sammy",
) {
id
email
fullName
dept
enrolled
}
}
Response
{
"data": {
"registerStudent": {
"id": "4",
"email": "contact@telixia.com",
"fullName": "Sammy",
"dept": null,
"enrolled": false
}
}
}
Note: You can choose which fields to return from the mutation by adding or removing fields within the curly braces following registerStudent. For example, if you wanted to only return the id and email you could simple omit the fullName, dept and enrolled field.
You have successfully created and tested the GraphQL server. In the next step, you will create a GitHub repository for the project.
Step 4 โ Creating the GitHub Repository
In this step, you will create a GitHub repository for your project and push your changes so that the GraphQL API can be automatically deployed from GitHub to any cloud platform of your choice.
Begin by initializing a repository from the prisma-graphql folder:
$ git init
To avoid committing the node_modules folder and the .env file, begin by creating a .gitignore file:
$ touch .gitignore
# .gitignore
# Specifies intentionally untracked files to ignore when using Git
# http://git-scm.com/docs/gitignore
.vscode/
npm-debug.log*
node_modules/
Next, use the following two commands to commit the code to the repository:
$ git add .
$ git commit -m 'Initial commit'
Now that the changes have been committed to your local repository, you will create a repository in GitHub and push your changes.
Go to GitHub to create a new repository. For consistency, name the repository node-graphql-lesson-03 and then click Create repository.
After the repository was created, push the changes with the following commands, which includes renaming the default local branch to main:
$ git remote add origin git@github.com:<USERNAME>/node-graphql-lesson-03.git
$ git branch -M main
$ git push --set-upstream origin main
You have successfully committed and pushed the changes to GitHub.
Step 6 โ Setting Up Prisma with PostgreSQL
So far the GraphQL API you built used the in-memory students array in the database.js file to store data. This means that if your server restarts, all changes to the data will be lost. To ensure that your data is safely persisted, you will replace the database.js with a PostgreSQL database and use Prisma to access the data.
In this step, you will install the Prisma CLI, create your initial Prisma schema, set up PostgreSQL locally with Docker, and connect Prisma to it.
The Prisma schema is the main configuration file for your Prisma setup and contains your database schema.
Begin by installing the Prisma CLI with the following command:
$ npm install prisma -D
The Prisma CLI will help with database workflows such as running database migrations and generating Prisma Client.
Next, youโll set up your PostgreSQL database using Docker. Create a new Docker Compose file with the following command:
$ touch docker-compose.yml
Now add the following code to the newly created file:
# node-graphql/docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:13
restart: always
environment:
- POSTGRES_USER=db_user
- POSTGRES_PASSWORD=db_password
volumes:
- postgres:/var/lib/postgresql/data
ports:
- '5432:5432'
volumes:
postgres:
This Docker Compose configuration file is responsible for starting the official PostgreSQL Docker image on your machine. The POSTGRES_USER and POSTGRES_PASSWORD environment variables set the credentials for the superuser (a user with admin privileges). You will also use these credentials to connect Prisma to the database. Finally, you define a volume where PostgreSQL will store its data, and bind the 5432 port on your machine to the same port in the Docker container.
With this setup in place, go ahead and launch the PostgreSQL database server with the following command:
$ docker-compose up -d
The Output:
Creating network "node-graphql_default" with the default driver
Creating volume "node-graphql_postgres" with default driver
Creating node-graphql_postgres_1 ... done
You can verify that the database server is running with the following command:
$ docker ps
This will output something similar to:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ca2813291692 postgres:13 "docker-entrypoint.sโฆ" 40 seconds ago Up 35 seconds 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp node-graphql_postgres_1
With the PostgreSQL container running, you can now create your Prisma setup. Run the following command from the Prisma CLI:
$ npx prisma init
Note that as a best practice, all invocations of the Prisma CLI should be prefixed with npx. This ensures itโs using your local installation.
After running the command, the Prisma CLI created a new folder called prisma in your project. It contains the following two files:
- schema.prisma: The main configuration file for your Prisma project (in which you will include your data model).
- .env: A dotenv file to define your database connection URL.
To make sure Prisma knows about the location of your database, open the prisma/.env file:
Adjust the DATABASE_URL environment variable to look as follows:
# node-graphql/prisma/.env
DATABASE_URL="postgresql://db_user:db_password@localhost:5432/college_db?schema=public"
Note that youโre using the database credentials db_user and db_password, which are specified in the Docker Compose file. To learn more about the format of the connection URL, visit the Prisma docs.
You have successfully started PostgreSQL and configured Prisma using the Prisma schema. In the next step, you will define your data model for the Student and use Prisma Migrate to create the database schema.
Step 7 โ Defining the Data Model with Prisma Migrate
Now you will define your data model in the Prisma schema file youโve just created. This data model will then be mapped to the database with Prisma Migrate, which will generate and send the SQL statements for creating the tables that correspond to your data model.
Since youโre building a college portal, the main entities of the application will be students, teachers and courses. In this step, you will define a Student model with a similar structure to the Student type in the GraphQL schema. In a later step, you will evolve the app and add a Teacher and Course models.
Note: The GraphQL API can be seen as an abstraction layer for your database. When building a GraphQL API, itโs common for the GraphQL schema to closely resemble your database schema. However, as an abstraction, the two schemas wonโt necessarily have the same structure, thereby allowing you to control which data you want to expose over the API. This is because some data might be considered sensitive or irrelevant for the API layer.
Prisma uses its own data modeling language to define the shape of your application data.
Go to node-graphql/prisma/schema.prisma Add the following model definitions to it:
//* node-graphql/prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Student {
id Int @id @default(autoincrement())
email String
fullName String
dept String?
enrolled Boolean @default(false)
}
You are defining a model called Student with a number of fields. The model will be mapped to a database table; the fields represent the individual columns.
The id fields have the following field attributes:
@default(autoincrement()) : This sets an auto-incrementing default value for the column.
@id: This sets the column as the primary key for the table.
With the model in place, you can now create the corresponding table in the database using Prisma Migrate. This can be done with the migrate dev command that creates the migration files and runs them.
Open up your terminal again and run the following command:
$ npx prisma migrate dev --name "init"
This will output something similar to:
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "college_db", schema "public" at "localhost:5432"
PostgreSQL database college_db created at localhost:5432
The following migration(s) have been created and applied from new schema changes:
migrations/
โโ 20210815160400_init/
โโ migration.sql
Your database is now in sync with your schema.
This command creates a new migration on your file system and runs it against the database to create the database schema. Hereโs a quick overview of the options that are provided to the command:
--name "init": Specifies the name of the migration (will be used to name the migration folder thatโs created on your file system).
--skip-generate: Skips generating Prisma Client (this will be done in the next step).
Your prisma/migrations directory is now populated with the SQL migration file. This approach allows you to track changes to the database schema and create the same database schema in production.
Note: If youโve already used Prisma Migrate with the college_db database and there is an inconsistency between the migrations in the prisma/migration folder and the database schema you will be prompted to reset the database with the following output:
Output
? We need to reset the PostgreSQL database "college_db" at "localhost:5432". All data will be lost.
Do you want to continue? โบ (y/N)
You can resolve this by entering y which will reset the database. Beware that this will cause all data in the database to be lost.
Youโve now created your database schema. In the next step, you will install Prisma Client and use it in your GraphQL resolvers.
Step 8 โ Using Prisma Client in the GraphQL Resolvers
Prisma Client is an auto-generated and type-safe Object Relational Mapper (ORM) that you can use to programmatically read and write data in a database from a Node.js application. In this step, youโll install Prisma Client in your project.
Open up your terminal again and install the Prisma Client npm package:
$ npm install @prisma/client
Note: Prisma Client gives you rich auto-completion by generating code based on your Prisma schema to the node_modules folder. To generate the code you use the npx prisma generate command. This is typically done after you create and run a new migration. On the first install, however, this is not necessary as it will automatically be generated for you in a postinstall hook.
With the database and GraphQL schema created, and Prisma Client installed, you will now use Prisma Client in the GraphQL resolvers to read and write data in the database. Youโll do this by replacing the content of the database.js, which youโve used so far to hold your data.
//* node-graphql/src/database.js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient();
module.exports = {
prisma,
}
Now you will update the Query resolvers to fetch enrolled students from the database. Update the resolvers.Query object with the following resolvers:
//* node-graphql/src/resolvers.js
const resolvers = {
Query: {
enrollment: (parent, args) => {
return prisma.student.findMany({
where: { enrolled: true },
});
},
student: (parent, args) => {
return prisma.student.findFirst({
where: { id: Number(args.id) },
});
},
},
Here, youโre using two Prisma Client queries:
findMany: Fetches students whose enrolled field is false.
findOne: Fetches a single student whose id field equals the id GraphQL argument.
Note, that as per the GraphQL specification, the ID type is serialized the same way as a String. Therefore you convert to a Number because the id in the Prisma schema is an int.
Next, you will update the Mutation resolver to save and update students in the database. Update the resolvers.Mutation Object with the following resolvers:
//* node-graphql/src/resolvers.js
const resolvers = {
...
Mutation: {
registerStudent: (parent, args) => {
return prisma.student.create({
data: {
email: args.email,
fullName: args.fullName,
},
});
},
enroll: (parent, args) => {
return prisma.student.update({
where: {
id: Number(args.id),
},
data: {
enrolled: true,
},
});
},
},
}
Final resolvers.js looks like this:
//* node-graphql/src/resolvers.js
const { prisma } = require("./database.js");
const Student = {
id: (parent, args, context, info) => parent.id,
email: (parent) => parent.email,
fullName: (parent) => parent.fullName,
dept: (parent) => parent.dept,
enrolled: (parent) => parent.enrolled,
};
const Query = {
enrollment: (parent, args) => {
return prisma.student.findMany({
where: { enrolled: true },
});
},
students: (parent, args) => {
return prisma.student.findMany({});
},
student: (parent, args) => {
return prisma.student.findFirst({
where: { id: Number(args.id) },
});
},
};
const Mutation = {
registerStudent: (parent, args) => {
return prisma.student.create({
data: {
email: args.email,
fullName: args.fullName,
dept: args.dept,
},
});
},
enroll: (parent, args) => {
return prisma.student.update({
where: { id: Number(args.id) },
data: {
enrolled: true,
},
});
},
};
const resolvers = { Student, Query, Mutation };
module.exports = {
resolvers,
};
Youโre using two Prisma Client queries:
create: Create a Student record.
update: Update the enrolled field of the Student record whose id matches the one in the query argument.
Now that youโve updated the resolvers to use Prisma Client, start the server to test the flow of data between the GraphQL API and the database with the following command:
$ npm start
Open the GraphQL playground at the address from the output and test the GraphQL API using the same queries from Step 3.
Then run the following two commands to commit the changes:
$ git add .
$ git commit -m 'Feature: Add Prisma'
$ git push
Run the migrations against the database with Prisma Migrate.
$ npx prisma migrate dev
Testing
Create Student
mutation {
registerStudent(email: "Olivia@telixia.com", fullName: "Olivia Catherine", dept: "Backend Engineer") {
id
fullName
dept
email
enrolled
}
}
Result
{
"data": {
"registerStudent": {
"id": "2",
"fullName": "Olivia Catherine",
"dept": "Backend Engineer",
"email": "Olivia@telixia.com",
"enrolled": false
}
}
}
Enroll Student
mutation {
enroll(id: 2) {
id
fullName
email
dept
enrolled
}
}
Result
{
"data": {
"enroll": {
"id": "2",
"fullName": "Olivia Catherine",
"email": "Olivia@telixia.com",
"dept": "Backend Engineer",
"enrolled": true
}
}
}
Query
query Query {
enrollment {
id
email
fullName
dept
enrolled
}
}
Result
{
"data": {
"enrollment": [
{
"id": "1",
"email": "felix@telixia.com",
"fullName": "Musah Felix",
"dept": null,
"enrolled": true
},
{
"id": "2",
"email": "Olivia@telixia.com",
"fullName": "Olivia Catherine",
"dept": "Backend Engineer",
"enrolled": true
}
]
}
}
Conclusion
Congratulations! You have completed the first part of Node Backend with Graphql and Prisma.
You will discover the full advantage in a more advance backend application with more than two models having a relationship.
A good example is having not only student, but also Teacher, Department, Courses Models added to our application.
Further Reading
[1] Lext Lesson to learn more on how to How To Build a GraphQL API with Apollo-Server and Prisma
[5] Introduction to Apollo Server
Happy Reading & Coding
๐ป ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐
Happy Reading & Coding
๐ป ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐ ๐
Posted on August 9, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.