DEV Community

Nditah
Nditah

Posted on

How To Build a GraphQL API with Apollo-Server and Prisma

Introduction

GraphQL is a query language designed to build client applications by providing an intuitive and flexible syntax and system for describing their data requirements and interactions. In the previous lesson, you learnt how use GraphQL and Prisma in combination as their responsibilities complement each other.
In this lesson, you learn how to work with multiple models having complex relationships that truly mirrors a business.

Content

πŸ”· Step 1 β€” Creating the Node.js Project

πŸ”· Step 2 β€” Setting Up Prisma with PostgreSQL

πŸ”· Step 3 β€” Creating and Migrating the Database with Prisma

πŸ”· Step 4 β€” Defining the GraphQL Schema

πŸ”· Step 5 β€” Defining the GraphQL Resolvers

πŸ”· Step 6 β€” Creating the GraphQL Server

πŸ”· Step 7 β€” Testing and Deployment

Prerequisites

πŸ”· Step 1 β€” Creating the Node.js Project

First, create a new directory for your project, initialize npm and install dependencies:

$ mkdir node-graphql-lesson-04

$ cd node-graphql-lesson-04

$ npm init --yes

$ npm install apollo-server graphql
Enter fullscreen mode Exit fullscreen mode
  • apollo-server: Apollo Server is a community-maintained open-source 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.

  • graphql: GraphQL.js is the JavaScript reference implementation for GraphQL. It provides two important capabilities: building a type schema and serving queries against that type schema

You have created your project and installed the dependencies. In the next step you define the GraphQL schema which determines the operations that the API can handle.

πŸ”· Step 2 β€” Setting Up Prisma with PostgreSQL

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now add the following code to the newly created file:

# node-graphql-lesson-04/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:

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

With the PostgreSQL container running, you can now create your Prisma setup. Run the following command from the Prisma CLI:

$ npx prisma init
Enter fullscreen mode Exit fullscreen mode
# node-graphql-lesson-04/prisma/.env

DATABASE_URL="postgresql://db_user:db_password@localhost:5432/college_db?schema=public"
Enter fullscreen mode Exit fullscreen mode

πŸ”· Step 3 β€” Creating and Migrating the Database with Prisma

Your GraphQL API for College has a single entity named Student. In this step, you’ll evolve the API by defining a new model in the Prisma schema and adapting the GraphQL schema to make use of the new model. You will introduce a Teacher, a Course and a Department model. Also, there exist a one-to-many relation from Department to the Student model as well as betwwen a Teacher, to a Course. This will allow you to represent the Teacher of Course and associate multiple Courses to each Teacher for instance. Then you will evolve the GraphQL schema to allow creating Teacher and associating Course with teachers through the API.

First, open the Prisma schema and add the following:

The college management system basically should have the following entities:

  • Students
  • Teachers
  • Departments
  • Courses

Other entities like Lessons, Fees, Marksheet, and Classes are obviously part of the solution, however, but for the sake of this lesson would not be necessary. See the entity diangram below:

Go to node-graphql/prisma/schema.prisma Add the following model definitions to it:

//* node-graphql-lesson-04/prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Student {
  id        Int        @id @default(autoincrement())
  email     String     @unique @db.VarChar(255)
  fullName  String?    @db.VarChar(255)
  enrolled  Boolean    @default(false)
  dept      Department @relation(fields: [deptId], references: [id])
  deptId    Int
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt

  @@map(name: "student")
}

model Department {
  id          Int       @id @default(autoincrement())
  name        String    @unique
  description String?   @db.VarChar(500)
  students    Student[]
  courses     Course[]
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  @@map(name: "department")
}

model Teacher {
  id        Int         @id @default(autoincrement())
  email     String      @unique @db.VarChar(255)
  fullName  String?     @db.VarChar(255)
  courses   Course[]
  type      TeacherType @default(FULLTIME)
  createdAt DateTime    @default(now())
  updatedAt DateTime    @updatedAt

  @@map(name: "teacher")
}

model Course {
  id          Int         @id @default(autoincrement())
  code        String      @unique
  title       String      @db.VarChar(255)
  description String?     @db.VarChar(500)
  teacher     Teacher?    @relation(fields: [teacherId], references: [id])
  teacherId   Int?
  dept        Department? @relation(fields: [deptId], references: [id])
  deptId      Int?
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt

  @@map(name: "course")
}

enum TeacherType {
  FULLTIME
  PARTTIME
}

Enter fullscreen mode Exit fullscreen mode

You’ve added the following to the Prisma schema:

  • The Department model to represent course Specialties.
  • The Teacher model to represent course Instructors/Facilitators.
  • The Course model to represent subject matters

The Student model has been modifies as follows:

  • Two relation fields: dept and deptId. Relation fields define connections between models at the Prisma level and do not exist in the database. These fields are used to generate the Prisma Client and to access relations with Prisma Client.

  • The deptId field, which is referenced by the @relation attribute. Prisma will create a foreign key in the database to connect Student and Department.

Note that the dept field in the Student model is optional, similarly to the teacher field in Course model. That means you’ll be able to create students unassociated with a department as well as a course without and associated Teacher.

The relationship makes sense because course are usually later on assigned to Teachers and also registered students are usually matriculated into a Department.

Next, create and apply the migration locally with the following command:

$  npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

If the migration succeeds you will receive the following:

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "college_db", schema "public" at "localhost:5432"

Database reset successful

The following migration(s) have been applied:

migrations/
  └─ 20210821201819_init/
    └─ migration.sql

βœ” Generated Prisma Client (2.29.1) to ./node_modules/@prisma/client in 109ms

Enter fullscreen mode Exit fullscreen mode

The command also generates Prisma Client so that you can make use of the new table and fields.

You will now update the GraphQL schema and resolvers to make use of the updated database schema.

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" 
Enter fullscreen mode Exit fullscreen mode

You’ve now created your database schema. Next, you will install Prisma Client.

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
Enter fullscreen mode Exit fullscreen mode

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-lesson-04/src/database.js

const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient();

module.exports = {
  prisma,
}
Enter fullscreen mode Exit fullscreen mode

Next, create a file database.js in your project src and add the students array to it as shown below:

πŸ”· Step 4 β€” Defining the GraphQL Schema

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. Create a src directory and inside it, create the schema.js file.

$ mkdir src
$ touch src/schema.js
Enter fullscreen mode Exit fullscreen mode

Now add the following code to the file:

//* node-graphql-lesson-04/src/schema.js

const { gql } = require("apollo-server")

const typeDefs = gql `

 type Student {
    id: ID!
    email: String!
    fullName: String!
    dept: Department!
    enrolled: Boolean
    updatedAt: String
    createdAt: String
  }

  type Department {
    id: ID!
    name: String!
    description: String
    students: [Student]
    courses: [Course]
    updatedAt: String
    createdAt: String
  }

  type Teacher {
    id: ID!
    email: String!
    fullName: String!
    courses: [Course]
    type: TeacherType
    updatedAt: String
    createdAt: String
  }

  type Course {
    id: ID!
    code: String!
    title: String!
    description: String
    teacher: Teacher
    dept: Department
    updatedAt: String
    createdAt: String
  }

  input TeacherCreateInput {
    email: String!
    fullName: String!
    courses: [CourseCreateWithoutTeacherInput!]
  }

  input CourseCreateWithoutTeacherInput {
    code: String!
    title: String!
    description: String
  }

  type Query {
    enrollment: [Student!]
    students: [Student!]
    student(id: ID!): Student
    departments: [Department!]!
    department(id: ID!): Department
    courses: [Course!]!
    course(id: ID!): Course
    teachers: [Teacher!]!
    teacher(id: ID!): Teacher
  }

  type Mutation {
    registerStudent(email: String!, fullName: String!, deptId: Int!): Student!
    enroll(id: ID!): Student
    createTeacher(data: TeacherCreateInput!): Teacher!
    createCourse(code: String!, title: String!, teacherEmail: String): Course!
    createDepartment(name: String!, description: String): Department!
  }

enum TeacherType {
  FULLTIME
  PARTTIME
}
`

module.exports = {
    typeDefs,
  }

Enter fullscreen mode Exit fullscreen mode

In this updated code, you’re adding the following changes to the GraphQL schema:

  • The Teacher type, which returns an array of Course.
  • The Department type, which returns an array of Student.
  • The Course type which has a Teacher type
  • The dept of type Department field to the Student type.
  • The createTeacher mutation, which expects the TeacherCreateInput as its input type.

  • The CourseCreateWithoutTeacherInput input type used in the TeacherCreateInput input for creating teachers as part of the createTeacher mutation.

  • The teacherEmail optional argument to the createCourse mutation.

With the schema created, you will now create the resolvers to match the schema.

πŸ”· Step 5 β€” Defining the GraphQL Resolvers

Make a sub-directory called resolvers inside the src directory.
Now, inside the resolvers create three files index.js, query.js, mutation.js as follows:

$ mkdir src/resolvers
$ touch src/resolvers/index.js
$ touch src/resolvers/query.js
$ touch src/resolvers/mutation.js
Enter fullscreen mode Exit fullscreen mode

Inside the mutation.js file, enter the following:

//* node-graphql-lesson-04/src/resolvers/mutation.js

const { prisma } = require("../database.js");

const Mutation = {
    registerStudent: (parent, args) => {
      return prisma.student.create({
        data: {
          email: args.email,
          fullName: args.fullName,
          dept: args.deptId && {
            connect: { id: args.deptId },
          },
        },
      });
    },
    enroll: (parent, args) => {
      return prisma.student.update({
        where: { id: Number(args.id) },
        data: {
          enrolled: true,
        },
      });
    },

    createTeacher: (parent, args) => {
      return prisma.teacher.create({
        data: {
          email: args.data.email,
          fullName: args.data.fullName,
          courses: {
            create: args.data.courses,
          },
        },
      });
    },

    createCourse: (parent, args) => {
      console.log(parent, args)
      return prisma.course.create({
        data: {
          code: args.code,
          title: args.title,
          teacher: args.teacherEmail && {
            connect: { email: args.teacherEmail },
          },
        },
      });
    },

    createDepartment: (parent, args) => {
      return prisma.department.create({
        data: {
          name: args.name,
          description: args.description,
        },
      });
    },
  };

  module.exports = {
    Mutation,
  }


Enter fullscreen mode Exit fullscreen mode

Inside the query.js file, enter the following:

//* node-graphql-lesson-04/src/resolvers/query.js

const { prisma } = require("../database.js");

const Query = {
    enrollment: (parent, args) => {
      return prisma.student.findMany({
        where: { enrolled: true },
      });
    },
    student: (parent, args) => {
      return prisma.student.findFirst({
        where: { id: Number(args.id) },
      });
    },

    students: (parent, args) => {
      return prisma.student.findMany({});
    },

    departments: (parent, args) => {
      return prisma.department.findMany({});
    },

    department: (parent, args) => {
      return prisma.department.findFirst({
        where: { id: Number(args.id) },
      });
    },

    courses: (parent, args) => {
      return prisma.course.findMany({});
    },

    course: (parent, args) => {
      return prisma.course.findFirst({
        where: { id: Number(args.id) },
      });
    },

    teachers: (parent, args) => {
      return prisma.teacher.findMany({});
    },

    teacher: (parent, args) => {
      return prisma.teacher.findFirst({
        where: { id: Number(args.id) },
      });
    },
  };

  module.exports = {
    Query,
  }

Enter fullscreen mode Exit fullscreen mode

And lastly, inside the index.js file, enter the following:

//* node-graphql-lesson-04/src/resolvers/index.js

const { prisma } = require("../database.js");
const { Query } = require("./query.js");
const { Mutation } = require("./mutation.js");

const Student = {
  id: (parent, args, context, info) => parent.id,
  email: (parent) => parent.email,
  fullName: (parent) => parent.fullName,
  enrolled: (parent) => parent.enrolled,
  dept: (parent, args) => {
    return prisma.department.findFirst({
      where: { id: parent.dept },
    });
  },
};

const Department = {
  id: (parent) => parent.id,
  name: (parent) => parent.name,
  description: (parent) => parent.description,
  students: (parent, args) => {
    return prisma.department.findUnique({
        where: { id: parent.id },
      }).students();
  },
  courses: (parent, args) => {
    return prisma.department.findUnique({
        where: { id: parent.id },
      }).courses();
  },
};

const Teacher = {
  id: (parent) => parent.id,
  email: (parent) => parent.email,
  fullName: (parent) => parent.fullName,
  courses: (parent, args) => {
    return prisma.teacher.findUnique({
        where: { id: parent.id },
      }).courses();
  },
};

const Course = {
  id: (parent) => parent.id,
  code: (parent) => parent.code,
  title: (parent) => parent.title,
  description: (parent) => parent.description,
  teacher: (parent, args) => {
    return prisma.course.findUnique({
        where: { id: parent.id },
      }).teacher();
  },
  dept: (parent, args) => {
    return prisma.course.findUnique({
      where: { id: parent.id },
    }).dept();
  },
};

const resolvers = {
  Student,
  Department,
  Teacher,
  Course,
  Query,
  Mutation,
};

module.exports = {
  resolvers,
};

Enter fullscreen mode Exit fullscreen mode

Let’s break down the changes to the resolvers:

  • The createCourse mutation resolver now uses the teacherEmail argument (if passed) to create a relation between the created course and an existing teacher.

  • The new createTeacher mutation resolver creates a teacher and related courses using nested writes.

  • The Teacher.courses and Post.teacher resolvers define how to resolve the courses and teacher fields when the Teacher or Post are queried. These use Prisma’s Fluent API to fetch the relations.

πŸ”· Step 6 β€” 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
Enter fullscreen mode Exit fullscreen mode

Now add the following code to the file:

//* node-graphql-lesson-04/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}`));
Enter fullscreen mode Exit fullscreen mode

Start the server to test the GraphQL API:

$  npm start
$  npm install nodemon -D
Enter fullscreen mode Exit fullscreen mode

Finally, your package.json file looks like:

{
  "name": "node-graphql-lesson-04",
  "version": "1.0.0",
  "description": "Graphql backend with node, prisma, postgres and docker",
  "main": "index.js",
  "scripts": {
    "start": "nodemon src/"
  },
  "keywords": [
    "Graphql",
    "Backend",
    "Prisma",
    "Postgre",
    "Docker",
    "Node.js"
  ],
  "author": "Nditah Sam <nditah@telixia.com>",
  "license": "ISC",
  "dependencies": {
    "@prisma/client": "^2.29.1",
    "apollo-server": "^3.1.2",
    "graphql": "^15.5.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.12",
    "prisma": "^2.29.1"
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ”· Step 7 β€” Testing and Deployment

Test the node-graphql-prisma backend by executing the following GraphQL queries and mutations:

Create Department

 mutation {
  createDepartment(name: "Backend Engineering", description: "Express, ApolloServer, Prisma, Docker, Postgres") {
    id
    name
    description

  }
}

mutation {
  createDepartment(name: "Frontend Development", description: "React, Angular, Vue, Gatsby, CSS, Bootstrap") {
    id
    name
    description
  }
}
Enter fullscreen mode Exit fullscreen mode

### Create Course


mutation CreateCourseMutation($createCourseCode: String!, $createCourseTitle: String!) {
  createCourse(code: $createCourseCode, title: $createCourseTitle) {
    id
    code
    title
    description
    teacher {
      id
      fullName
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

### Create Teacher

mutation CreateTeacherMutation($createTeacherData: TeacherCreateInput!) {
  createTeacher(data: $createTeacherData) {
    id
    fullName
    createdAt
    courses {
      id
      code
      title
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that you can fetch the teacher whenever the return value of a query is Course. In this example, the Course.teacher resolver will be called.

Finally, commit your changes and push to deploy the API:

$  git add .
$  git commit -m "Feature: Add Teacher, Couse, Department"
$  git push
Enter fullscreen mode Exit fullscreen mode

You have successfully evolved your database schema with Prisma Migrate and exposed the new model in your GraphQL API.
The Github repository of this project can be found here.

Conclusion

Even though this lesson is not meant to compare REST vs. Graphql, it should be highlighted that:

πŸ”· While GraphQL simplifies data consumption, REST design standards are strongly favoured by many sectors due to cache-ability features, security, tooling community and ultimate reliability. For this reason and its storied record, many web services favour REST design.

πŸ”· Regardless of their choice, backend developers must understand exactly how frontend users will interact with their APIs to make the correct design choices. Though some API styles are easier to adopt than others, with the right documentation and walk-throughs in place, backend engineers can construct a high-quality API platform that frontend developers will love, no matter what style is used.

Further Reading

[1] Prisma Fluent-Api
[2] Prisma Components
[3] Introduction to GraphQl
[4] Introduction to Apollo Server

Happy Reading & Coding

πŸ’» πŸ““ πŸ“• πŸ“— πŸ“˜ πŸ“™ πŸ“” πŸ“’ πŸ“š πŸ“– πŸ’™ πŸ’œ πŸ’“ πŸ’— πŸ’– πŸ’˜ πŸ’ 🎁 🎊 πŸŽ‰

Discussion (7)

Collapse
wendumaris profile image
Nwafor Stellamaris Chinwendu

Great ArticleπŸ”₯
Thank you Sir..

Collapse
nditah profile image
Nditah Author

I appreciate the kind words.

Collapse
youngmikec profile image
youngmikec

This is very informative. I like this article. However, I think graphql will take a longer time to grasp than REST API.

Collapse
nditah profile image
Nditah Author

Thanks for your support.

Collapse
yahnic profile image
Yahnic

Thanks for the lesson.

Collapse
noleykuz profile image
Noleykuz

nice work boss. looking forward catch up with it soon

Collapse
patriciorivera profile image
Patricio Rivera

Hi Nditah and thanks for your tutorial. A question: why are you using apollo-server package instead of apollo-server-express? Thanks in advance.