September 27, 2021

Fullstack App With TypeScript, PostgreSQL, Next.js, Prisma & GraphQL: GraphQL API

This article is the second part of a course where you build a fullstack app with Next.js, GraphQL, TypeScript, Prisma, and PostgreSQL. In this article, you will create the GraphQL API and interact with it on the frontend.

Part 1
Fullstack App With TypeScript, PostgreSQL, Next.js, Prisma & GraphQL: Data Modeling

Table of contents

Introduction

In this course you will learn how to build "awesome-links", a fullstack app where users can browse through a list of curated links and bookmark their favorite ones.

In the last part, you used Prisma to set up the database layer. By the end of this part, you will learn about GraphQL: what it is and how you can use it to build an API in a Next.js app.

Development environment

To follow along with this tutorial, you need to have Node.js and the GraphQL extension installed. You will also need to have a running PostgreSQL instance.

Note: you can set up PostgreSQL locally or a hosted instance on Heroku. Note that you will need a remote database for the deployment step at the end of the course.

Clone the repository

You can find the complete source code for the course on GitHub.

Note: Each article has a corresponding branch. This way, you can follow along as you go through it. By checking out the part-2 branch, you have the same starting point as this article.

To get started, navigate into the directory of your choice and run the following command to clone the repository.

git clone -b part-2 https://github.com/m-abdelwahab/awesome-links.git
Copy

You can now navigate into the cloned directory, install the dependencies and start the development server:

cd awesome-links
npm install
npm run dev
Copy

The app will be running at http://localhost:3000/ and you will see four items. The data is hardcoded and comes from the /data/links.ts file.

What the starter project looks likeWhat the starter project looks like

Seeding the database

After setting up a PostgreSQL database, rename the env.example file to .env and set the connection string for your database. After that, run the following command to create the tables in your database:

npx prisma db push
Copy

Next, run the following command to seed the database:

npx prisma db seed --preview-feature
Copy

This command will run the seed.ts script, located in the /prisma directory. This script adds four links and one user to your database using Prisma Client.

A look at the project structure and dependencies

You will see the following folder structure

awesome-links/
┣ components/
┃ ┣ Layout/
┃ ┗ AwesomeLink.tsx
┣ data/
┃ ┗ links.ts
┣ pages/
┃ ┣ _app.tsx
┃ ┣ about.tsx
┃ ┗ index.tsx
┣ prisma/
┃ ┣ schema.prisma
┃ ┗ seed.ts
┣ public/
┣ styles/
┃ ┗ tailwind.css
┣ .env.example
┣ .gitignore
┣ README.md
┣ next-env.d.ts
┣ package-lock.json
┣ package.json
┣ postcss.config.js
┣ tailwind.config.js
┗ tsconfig.json

This is a Next.js application with TailwindCSS set up along with Prisma.

In the pages directory, you will find three files:

  • _app.tsx: the global App component, which is used to add a navigation bar that persists between page changes and to add global CSS.
  • about.tsx: this file exports a React component which renders a page located at http://localhost:3000/about.
  • index.tsx: the home page, which contains a list of links. These links are hardcoded in the /data/links.ts file.

Next, you will find a prisma directory which contains the following files:

  • schema.prisma: the schema of our database, written in PSL (Prisma Schema Language). If you want to learn how the database was modeled for this app, check out the last part of the course.
  • seed.ts: script that will seed the database with dummy data.

Buiding APIs the traditional way: REST

In the last part of the course, you set up the database layer using Prisma. The next step is to build the API layer on top of the data model, which will allow you to request or send data from the client.

A common approach to structure the API is to have the client send requests to different URL endpoints. The server will retrieve or modify a resource based on the request type and send back a response. This architectural style is known as REST, and it has a couple of advantages:

  • Flexible: an endpoint can handle different types of requests
  • Cacheable: all you need to do is cache the response of a specific endpoint
  • Separation between the client and the server: different platforms (for example, web app, mobile app, etc.) can consume the API.

REST APIs and their drawbacks

While REST APIs offer advantages, they also have some drawbacks. We will use awesome-links as an example.

Here is one possible way of structuring the REST API of awesome-links:

ResourceHTTP MethodRouteDescription
UserGET/usersreturns all users and their information
UserGET/users/:idreturns a single user
LinkGET/linksreturns all links
LinkGET, PUT, DELETE/links/:idreturns a single link, updates it or deletes it. id is the link's id
UserGET/favoritesreturns a user's bookmarked links
UserPOST/link/saveadds a link to the user's favorites
LinkPOST/link/newcreates a new link (done by admin)

Each REST API is different

Another developer may have structured their REST API differently, depending on how they see fit. This flexibility comes with a cost: every API is different.

This means every time you work with a REST API, you will need to go through its documentation and learn about:

  • The different endpoints and their HTTP methods.
  • The request parameters for each endpoint.
  • What data and status codes are returned by every endpoint.

This learning curve adds friction and slows down developer productivity when working with the API for the first time.

On the other hand, backend developers who built the API need to manage it and maintain its documentation.

And when an app grows in complexity, so does the API: more requirements lead to more endpoints created.

This increase in endpoints will most likely introduce two issues: overfetching and underfetching data.

Overfetching and underfetching

Overfetching occurs when you fetch more data than you need. This leads to slower performance since you are consuming more bandwidth.

On the other hand, sometimes you find that an endpoint does not return all the necessary to be displayed in the UI, so you end up making one or more requests to another endpoint. This also leads to slow performance since there will be a waterfall of network requests that need to occur.

In the "awesome-links" app, if you want a page to display all users and their links, you will need to make an API call to the /users/ endpoint and then make another request to /favorites to fetch their favorites.

Having the /users endpoint return users and their favorites will not solve the problem. That is because you will end up with a significant API response that will take a long time to load.

REST APIs are not typed

Another downside about REST APIs is they are not typed. You do not know the types of data returned by an endpoint nor what type of data to send. This leads to making assumptions about the API, which can lead to bugs or unpredictable behavior.

For example, do you pass the user id as a string or a number when making a request? Which request parameters are optional, and which ones are required? That is why you will use the documentation, however, as an API evolves, documentation can get outdated. There are solutions that address these challenges, but we will not cover them in this course.

GraphQL, an alternative to REST

GraphQL is a new API standard that was developed and open-sourced by Facebook. It provides a more efficient and flexible alternative to REST, where a client can receive exactly the data it needs.

Instead of sending requests to one or more endpoints and stitching the responses, you only send requests to a single endpoint.

Here is an example of a GraphQL query that returns all links in the "awesome-links" app. You will define this query later when building the API:

query {
links {
id
title
}
}

Example of GraphQL queryExample of GraphQL query

The API only returns the id and title, even though a link has more fields.

Note: this is Apollo Studio Explorer, a playground for running GraphQL operations. It offers nice features which we will cover in more detail

Now you will see how you can get started with building a GraphQL API.

Defining a schema

It all starts with a GraphQL schema where you define all operations that your API can do. You also specify the operations' input arguments along with the response type.

This schema acts as the contract between the client and the server. It also serves as documentation for developers consuming the GraphQL API. You define the schema using GraphQL's SDL (Schema Definition Langauge).

Let's look at how you can define the GraphQL schema for the "awesome-links" app.

Defining object types and fields

The first thing you need to do is define an Object type. Object types represent a kind of object you can fetch from your API.

Each object type can have one or many fields. Since you want to have users in the app, you will need to define a User object type:

type User {
id: String
name: String
email: String
image: String
role: Role
bookmarks: [Link]
}
enum Role {
ADMIN
USER
}

The User type has the following fields:

  • email, which is of type String.
  • id, which is of type String.
  • name, which is of type String.
  • image, which is of type String.
  • role, which is of type Role. This is an enum, which means a user's role can take one of two values: either USER or ADMIN.
  • bookmarks, which is an array of type Link. Meaning a user can have many links. You will define the Link object next.

This is the definition for the Link object type:

type Link {
id: String
category: String
description: String
imageUrl: String
title: String
url: String
users: [User]
}

This is a many-to-many relation between the Link and User object types since a Link can have many users, and a User can have many links. This is modeled in the database using Prisma.

Defining Queries

To fetch data from a GrahQL API, you need to define a Query Object type. This is a type where you define an entry point of every GraphQL query. For each entry point, you define its arguments and its return type.

Here is a query that returns all links.

type Query {
links: [Link]!
}

The links query returns an array of type Link. The ! is used to indicate that this field is non-nullable, meaning that the API will always return a value when this field is queried.

You can add more queries depending on the type of API you want to build. For the "awesome-links" app, you can add a query to return a single link, another one to return a single user, and another to return all users.

type Query {
links: [Link]!
link(id: String!): Link!
user(id: String!): User!
users: [User]!
}
  • The link query takes an argument id of type String and returns a Link. The id argument is required, and the response is non-nullable.
  • The user query takes an argument id of type String and returns a User. The id argument is required, and the response is non-nullable.
  • The users query returns an array of type User. The id argument is required. The response is non-nullable.

Defining mutations

To create, update or delete data, you need to define a Mutation Object type. It is a convention that any operations that cause writes should be sent explicitly via a mutation. In the same way, you should not use GET requests to modify data.

For the "awesome-links" app, you will need different mutations for creating, updating and deleting a link:

type Mutation {
createLink(category: String!, description: String!, imageUrl: String!, title: String!, url: String!): Link!
deleteLink(id: String!): Link!
updateLink(category: String, description: String, id: String, imageUrl: String, title: String, url: String): Link!
}
  • The createLink mutation takes as an argument a category, a description, a title, a url and an imageUrl. All of these fields are of type String and are required. This mutation returns a Link object type.
  • The deleteLink mutation takes as an id of type String as a required argument. It returns a required Link.
  • The updateLink mutation takes the same arguments as the createLink mutation. However, arguments are optional. This way, when updating a Link you will only pass the fields you want to be updated. This mutation returns a required Link.

Defining the implementation of queries and mutations

So far, you only defined the schema of the GraphQL API, but you haven't specified what should happen when a query or a mutation runs. The functions responsible for executing the implementation of the query or a mutation are called resolvers. Inside the resolvers, you can send queries to a database or a request to a 3rd-party API.

For this tutorial, you will use Prisma inside the resolvers to send queries to a PostgreSQL database.

Building the GraphQL API

To build the GraphQL API, you will need a GraphQL server that will serve a single endpoint.

This server will contain the GraphQL schema along with the resolvers. For this project, you will use Apollo Server.

To get started, in the starter repo you cloned in the beginning, run the following command in your terminal:

npm install graphql apollo-server-micro micro-cors
Copy

The graphql package is the JavaScript reference implementation for GraphQL. It is a peer-dependency for apollo-server-micro, a Micro integration for Apollo Server. This integration is optimized for Next.js. Finally, you are using micro-cors to be able to use Apollo Studio.

Defining the schema of the app

Next, you need to define the GraphQL schema. Create a new graphql directory in the project's root folder, and inside it, create a new schema.ts file. You will define the Link object along with a query that returns all links.

// graphql/schema.ts
import { gql } from 'apollo-server-micro'
export const typeDefs = gql`
type Link {
id: String
title: String
description: String
url: String
category: String
imageUrl: String
users: [String]
}
type Query {
links: [Link]!
}
`
Copy

The gql tag is a template literal tag for wrapping GraphQL strings, such as schema definitions. It enables syntax highlighting and converts GraphQL strings into the format that Apollo libraries expect when working with operations and schemas.

Defining resolvers

The next thing you need to do is create the resolver function for the links query. To do so, create a /graphql/resolvers.ts file and add the following code:

export const resolvers = {
Query: {
links: () => {
return [
{
category: 'Open Source',
description: 'Fullstack React framework',
id: '8a9020b2-363b-4a4f-ad26-d6d55b51bqes',
imageUrl: 'https://nextjs.org/static/twitter-cards/home.jpg',
title: 'Next.js',
url: 'https://nextjs.org',
},
{
category: 'Open Source',
description: 'Next Generation ORM for TypeScript and JavaScript',
id: '2a3121b2-363b-4a4f-ad26-d6c35b41bade',
imageUrl: 'https://www.prisma.io/images/og-image.png',
title: 'Prisma',
url: 'https://prisma.io',
},
{
category: 'Open Source',
description: 'GraphQL implementation',
id: '2ea8cfb0-44a3-4c07-bdc2-31ffa135ea78',
imageUrl: 'https://www.apollographql.com/apollo-home.jpg',
title: 'Apollo GraphQL',
url: 'https://apollographql.com',
},
]
},
},
}
Copy

resolvers is an object where you will define the implementation for each query and mutation. The functions inside the Query object must match the names of the queries defined in the schema. Same thing goes for mutations. Here the links resolver function returns an array of objects, where each object is of type Link.

Creating the GraphQL endpoint

To create the GraphQL endpoint, you will leverage Next.js' API routes. Any file inside the /pages/api folder is mapped to a /api/* endpoint and treated as an API endpoint.

Go ahead and create a /pages/api/graphql.ts file and add the following code:

import { ApolloServer } from 'apollo-server-micro'
import { typeDefs } from '../../graphql/schema'
import { resolvers } from '../../graphql/resolvers'
import Cors from 'micro-cors'
const cors = Cors()
const apolloServer = new ApolloServer({ typeDefs, resolvers })
const startServer = apolloServer.start()
export default cors(async function handler(req, res) {
if (req.method === 'OPTIONS') {
res.end()
return false
}
await startServer
await apolloServer.createHandler({
path: '/api/graphql',
})(req, res)
})
export const config = {
api: {
bodyParser: false,
},
}
Copy

You created a new apolloServer instance which takes the schema and resolvers you just created as a parameter. You then created a function named startServer, which calls apolloServer.start();. This is a requirement of Apollo Server 3.

You then defined an asynchronous function called handler, which takes a request and response object. Inside the body of this function, you are first calling the startServer function and then the createHandler function while setting the path to /api/graphql. This is the endpoint of the GraphQL API.

Finally, every API route can export a config object to change the default configs. Body parsing is disabled here since it is handled by GraphQL.

Sending queries using GraphQL playground

After completing the previous steps, start the server by running the following command:

npm run dev
Copy

When navigating to http://localhost:3000/api/graphql/, you should see the following page:

Apollo Studio asking if you are ready to start querying your GraphApollo Studio asking if you are ready to start querying your Graph

Clicking on "Query your server" will redirect you to Apollo Studio Explorer.

Apollo Studio showing us the query playgroundApollo Studio showing us the query playground

GraphQL Playground allows you to explore a GraphQL API by specifying an endpoint. Since you are working locally, the endpoint is http://localhost:3000/api/graphql/.

In the schema tab placed on the left (the first icon under the apollo logo), you can see the entire GraphQL schema. You can also explore each query/mutation individually, seeing the different needed arguments along with their types.

To make sure that everything runs as expected, write the links query and specify the different fields you want returned and click on the "Query" button. In the response section to the right, you should be able to see the hardcoded data you wrote earlier in the links resolver.

Initialize Prisma Client

So far, the GraphQL API returns hardcoded data in the resolvers function. You will use Prisma Client in these functions to send queries to the database.

Prisma Client is an auto-generated, type-safe, query builder. To be able to use it in your project, you should instantiate it once and then reuse it across the entire project. Go ahead and create a /lib folder in the project's root folder and inside it create a prisma.ts file. Next, add the following code to it:

// /lib/prisma.ts
import { PrismaClient } from '@prisma/client'
// PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit.
// Learn more: https://pris.ly/d/help/next-js-best-practices
let prisma: PrismaClient
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
export default prisma
Copy

First, you are creating a new Prisma Client instance. Then if you are not in a production environment, Prisma will be attached to the global object so that you do not exhaust the database connection limit. For more details, check out the documentation for Next.js and Prisma CLient best practices.

Creating a GraphQL context

The next step is to create a context so that the resolvers have access to the Prisma Client and be able send queries to the database.

Go ahead and create a context.ts file inside the /graphql folder and add the following code to it:

// /graphql/context.ts
import { PrismaClient } from '@prisma/client'
import prisma from '../lib/prisma'
export type Context = {
prisma: PrismaClient
}
export async function createContext({ req, res }): Promise<Context> {
return {
prisma,
}
}
Copy

You are creating a Context type and attaching the PrismaClient type to it. Next, you are exporting an asynchronous function called createContext() that returns the prisma instance created in the lib directory.

Now update the /pages/api/graphql.ts file to include the context:

// /pages/api/graphql.ts
import { ApolloServer } from 'apollo-server-micro';
import { typeDefs } from '../../graphql/schema';
import { resolvers } from '../../graphql/resolvers';
+ import { createContext } from '../../graphql/context';
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
+ context: createContext,
});
const startServer = apolloServer.start();
export default cors(async function handler(req, res) {
if (req.method === 'OPTIONS') {
res.end();
return false;
}
await startServer;
await apolloServer.createHandler({
path: '/api/graphql',
})(req, res);
});
export const config = {
api: {
bodyParser: false,
},
};
Copy

Now you can update the resolver to return data from the database. Inside the /graphql/resolvers.ts file, update the `links function to the following code:

// /graphql/resolvers.ts
export const resolvers = {
Query: {
links: (_parent, _args, ctx) => {
return ctx.prisma.link.findMany()
},
},
}
Copy

The resolver now has three optional arguments:

  • _parent: the return value of the resolver for this field's parent. Since it's not being used in the resolver, you are prefixing with an underscore.
  • _args: an object that contains all GraphQL arguments provided for a field. For example, when executing query { link(id: "4") }, the args object passed to the user resolver is { "id": "4" }. Since it's not being used in the resolver, you are prefixing with an underscore.
  • The context argument is helpful for passing things that any resolver might need, like authentication scope, database connections, and custom fetch functions. Here we are using it to access Prisma Client.

If everything is set up correctly, when going to Apollo Studio Explorer at http://localhost:3000/api/graphql and you run the links query, the returned data will be coming from your database.

The flaws with our current GraphQL setup

When the GraphQL API grows in complexity, the current workflow of creating the schema and the resolvers manually can decrease developer productivity:

  • Resolvers must match the same structure as the schema and vice-versa. Otherwise, you can end up with buggy and unpredictable behavior. These two components can accidentally go out of sync when the schema evolves or the resolver implementation changes.
  • The GraphQL schema is defined as strings, so no auto-completion and build-time error checks for the SDL code.

To solve these problems, one can use a combination of tools like GraphQL code-generator. Alternatively, you can use a code-first approach when building the schema with its resolvers.

Code-first GraphQL APIs using Nexus

Nexus is a GraphQL schema construction library where you define your GraphQL schema using code. The value proposition of this approach is you are using a programming language to build your API, which has multiple benefits:

  • No need to context-switch between SDL and the programming language you are using to build your business logic.
  • Auto-completion from the text-editor
  • Type-safety (if you are using TypeScript)

These benefits contribute to a better development experience with less friction.

To get started, run the following command to install Nexus:

npm install nexus
Copy

Next, in the /graphql/schema.ts file replace the typeDefs with the following code, which will create an empty graphql schema:

// /graphql/schema.ts
import { makeSchema } from 'nexus'
import { join } from 'path'
export const schema = makeSchema({
types: [],
outputs: {
typegen: join(process.cwd(), 'node_modules', '@types', 'nexus-typegen', 'index.d.ts'),
schema: join(process.cwd(), 'graphql', 'schema.graphql'),
},
contextType: {
export: 'Context',
module: join(process.cwd(), 'graphql', 'context.ts'),
},
})
Copy

You are importing the makeSchema() function from Nexus, which takes an object as an argument. Inside this object you are including the following fields:

  • types: an array which will include all of the different object types.
  • outputs: an object where you specify the location of the generated types of your GraphQL API and the schema which is written in SDL. Here the types will be generated in a file located in the node_modules directory and the schema will be generated in the /graphql directory.
  • contextType: an object for including the context type. You are importing the exported Context type, defined in /graphql/context.ts file.

Finally, update the import in the /pages/api/graphql.ts file:

// /pages/api/graphql.ts
import { ApolloServer } from 'apollo-server-micro';
+ import { schema } from '../../graphql/schema';
import { resolvers } from '../../graphql/resolvers';
import { createContext } from '../../graphql/context';
const apolloServer = new ApolloServer({
+ schema,
resolvers,
context: createContext,
});
// code below unchanged
Copy

Make sure the server is running and navigate to http://localhost:3000/api/graphql. You will be able to send a query with an ok field, which will return true

QueryQuery

Defining the schema using Nexus

The first step is defining a Link object type using Nexus. Go ahead and create a /graphql/types/Link.ts file, add the following code:

// /graphql/types/Link.ts
import { objectType, extendType } from 'nexus'
import { User } from './User'
export const Link = objectType({
name: 'Link',
definition(t) {
t.string('id')
t.string('title')
t.string('url')
t.string('description')
t.string('imageUrl')
t.string('category')
t.list.field('users', {
type: User,
async resolve(_parent, _args, ctx) {
return await ctx.prisma.link
.findUnique({
where: {
id: _parent.id,
},
})
.users()
},
})
},
})
Copy

The Link object type is created using the objectType() function from Nexus. This function takes as an argument an object where you specify the name of the object type and the different fields. You also specify the type of each field. The id, title, url, description, imageUrl, category are of type string. For the field, you are specifying that it is an array of typeUser`.

Now create a new /graphql/types/User.ts file and add the following to code to create the User type:

// /graphql/types/User.ts
import { enumType, objectType } from 'nexus'
import { Link } from './Link'
export const User = objectType({
name: 'User',
definition(t) {
t.string('id')
t.string('name')
t.string('email')
t.string('image')
t.field('role', { type: Role })
t.list.field('bookmarks', {
type: Link,
async resolve(_parent, _args, ctx) {
return await ctx.prisma.user
.findUnique({
where: {
id: _parent.id,
},
})
.bookmarks()
},
})
},
})
const Role = enumType({
name: 'Role',
members: ['USER', 'ADMIN'],
})
Copy

To make imports more manageable when adding more types, create a graphql/types/index.ts file, which will be used as an index to re-export all types from your schema. This way, you can import all types at once.

// graphql/types/index.ts
export * from './Link'
export * from './User'
Copy

Now in the graphql/schema.ts update the import, to include the types you just created.

// graphql/schema.ts
import { makeSchema } from 'nexus'
import { join } from 'path'
+ import * as types from './types'
export const schema = makeSchema({
+ types,
outputs: {
typegen: join(process.cwd(), 'node_modules', '@types', 'nexus-typegen', 'index.d.ts'),
schema: join(process.cwd(), 'graphql', 'schema.graphql'),
},
contextType: {
export: 'Context',
module: join(process.cwd(), 'graphql', 'context.ts'),
},
})
Copy

Defining queries using Nexus

In the graphql/types/Link.ts file, add the following code below the Link object type definition:

// graphql/types/Link.ts
// code above unchanged
export const LinksQuery = extendType({
type: 'Query',
definition(t) {
t.nonNull.list.field('links', {
type: 'Link',
resolve(_parent, _args, ctx) {
return ctx.prisma.link.findMany()
},
})
},
})
Copy

You are using the extendType() function from Nexus to create a query.

  • .nonNull specifies that clients will always get a value for this field. In Nexus, all "output types" (types returned by fields) are nullable by default.
  • .list specifies that this query will return a list.
  • field() is a function that takes two arguments:
    • The field's name. In this case, the query is called "links" like the schema you created initially.
    • An object that specifies the returned type of the query along with the resolver function. In the resolver function, you are accessing prisma from the context and using the findMany() function to return all links in the Link table of the database.

Now if you go back to the Apollo Studio Explorer, you will be able to send a query that returns all links from the database.

Nexus API query responseNexus API query response

Client-side GraphQL queries

For this project, you will be using Apollo Client. You can send a regular HTTP POST request to interact with the GraphQL API you just built. However, you get a lot of benefits when using a GrapQL Client instead.

Apollo Client takes care of requesting and caching your data, as well as updating your UI. It also includes features for query batching, query deduplication, and pagination.

Setting up Apollo Client in Next.js

To get started with Apollo Client, add to your project by running the following command:

npm install @apollo/client
Copy

Next, in the /lib directory create a new file called apollo.ts and add the following code to it:

// /lib/apollo.ts
import { ApolloClient, InMemoryCache } from '@apollo/client'
const apolloClient = new ApolloClient({
uri: 'http://localhost:3000/api/graphql',
cache: new InMemoryCache(),
})
export default apolloClient
Copy

You are creating a new ApolloClient instance to which you are passing a configuration object with uri and cache fields.

  • The uri field specifies the GraphQL endpoint you will interact with. This will be changed to the production URL when the app is deployed.
  • The cache field is an instance of InMemoryCache, which Apollo Client uses to cache query results after fetching them.

Next, go to the /pages/_app.tsx file and add the following code to it, which sets up Apollo Client:

// /pages/_app.tsx
import '../styles/tailwind.css'
import Layout from '../components/Layout'
import { ApolloProvider } from '@apollo/client'
import apolloClient from '../lib/apollo'
function MyApp({ Component, pageProps }) {
return (
<ApolloProvider client={apolloClient}>
<Layout>
<Component {...pageProps} />
</Layout>
</ApolloProvider>
)
}
export default MyApp
Copy

You are wrapping the global App component with the Apollo Provider so all of the project's components can send GraphQL queries.

Note: Next.js supports different data fetching strategies. You can fetch data server-side, client-side, or at build-time. To support pagination, you need to fetch data client-side.

Sending requests using useQuery

To load data on your frontend using Apollo client, update the /pages/index.tsx file to use the following code:

// /pages/index.tsx
import Head from 'next/head'
import { gql, useQuery } from '@apollo/client'
const AllLinksQuery = gql`
query {
links {
id
title
url
description
imageUrl
category
}
}
`
export default function Home() {
const { data, loading, error } = useQuery(AllLinksQuery)
if (loading) return <p>Loading...</p>
if (error) return <p>Oh no... {error.message}</p>
return (
<div>
<Head>
<title>Awesome Links</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="container mx-auto max-w-5xl my-20">
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{data.links.map(link => (
<li key={link.id} className="shadow max-w-md rounded">
<img className="shadow-sm" src={link.imageUrl} />
<div className="p-5 flex flex-col space-y-2">
<p className="text-sm text-blue-500">{link.category}</p>
<p className="text-lg font-medium">{link.title}</p>
<p className="text-gray-600">{link.description}</p>
<a href={link.url} className="flex hover:text-blue-500">
{link.url.replace(/(^\w+:|^)\/\//, '')}
<svg
className="w-4 h-4 my-1"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" />
<path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" />
</svg>
</a>
</div>
</li>
))}
</ul>
</div>
</div>
)
}
Copy

You are using the useQuery hook to send queries to the GraphQL endpoint. This hook has a required parameter of a GraphQL query string. When the component renders, useQuery returns an object which contains three values:

  • loading: a boolean that determines whether or not the data has been returned.
  • error: an object that contains the error message in case an error occurs after sending the query.
  • data: contains the data returned from the API endpoints.

After you save the file and you navigate to http://loclahost:3000, you will see a list of links which are fetched from the database.

Pagination

AllLinksQuery returns all the links you have in the database. As the app grows and you add more links, you will have a large API response that will take a long time to load. Also the database query sent by the resolver will become slower, since you are returning the links in the database using the prisma.link.findMany() fucntion.

A common approach to improve performance is to add support for pagination. This is when you split a large data set into smaller chunks that can be requested as needed.

There are different ways to implement pagination. You can do numbered pages, for example like Google search results, or you can do infinite scrolling like Twitter's feed.

Infinite scolling GIF by https://dribbble.com/artraydInfinite scolling GIF by https://dribbble.com/artrayd

Pagination at the database level

Now at the database level, there are two pagination techniques that you can use: offset-based and cursor-based pagination.

  • Offset-based: you skip a certain number of results and select a limited range. For example, you can skip the first 200 results and take only 10 after. The downside of this approach is that it does not scale at the database level. If for example you skip the first 200,000 records, the database still has to traverse all of them, which will affect performance.

For more information on why you may want to use off-set based pagination, check out the documentation.

Offset-based paginationOffset-based pagination

  • Cursor-based pagination: you use a cursor to bookmark a location in a result set. On subsequent requests you can then jump straight to that saved location. Similar to how you can access an array by its index.

The cursor must be a unique, sequential column - such as an ID or a timestamp. This approach is more efficient than offset-based pagination and will be the one you use in this tutorial.

Cursor-based paginationCursor-based pagination

Pagination in GraphQL

To make the GraphQL API support pagination, you need to introduce Relay Cursor Connections Specification to the GraphQL schema. This is a specification for how a GraphQL server should expose paginated data.

Here is what the paginated query of allLinksQuery will look like:

query allLinksQuery($first: Int, $after: String) {
links(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
id
imageUrl
title
description
url
category
}
}
}
}

The query takes two arguments, first and after:

  • first: an Int that specifies how many items you want the API to return.
  • after: a String argument that bookmarks the last item in a result set, this is the cursor.

This query returns an object containing two fields, pageInfo and edges:

  • pageInfo:an object that helps the client determine if there is more data to be fetched. This object contains two fields, endCursor and hasNextPage:
    • endCursor: the cursor of the last item in a result set. This cursor is of type String
    • hasNextPage: a boolean returned by the API that lets the client know if there are more pages that can be fetched.
  • edges is a an array of objects, where each object has a cursor and a node fields. The node field here returns the Link object type.

You will implement one-way pagination, where some links are requested when the page first loads, then the user can fetch more by clicking a button.

Alternatively, you can make this request as the user reaches the end of the page when scrolling.

The way this works is that you fetch some data as the page first loads. Then after clicking a button, you send a second request to the API which includes how many items you want returned and a cursor. The data is then returned and displayed on the client.

How Pagination works client-sideHow Pagination works client-side

Note: an example of two-way pagination is a chat app like Slack, where you can load messages by going forwards or backwards.

Modifying the GraphQL schema

To recreate this using Nexus, navigate to the /graphql/types/Link.ts file and add the following code:

// /graphql/types/Link.ts
// code above unchanged
export const Edge = objectType({
name: 'Edge',
definition(t) {
t.string('cursor')
t.field('node', {
type: Link,
})
},
})
export const PageInfo = objectType({
name: 'PageInfo',
definition(t) {
t.string('endCursor')
t.boolean('hasNextPage')
},
})
export const Response = objectType({
name: 'Response',
definition(t) {
t.field('pageInfo', { type: PageInfo })
t.list.field('edges', {
type: Edge,
})
},
})
Copy

You defined the following object types:

  • Egdes: contains a cursor field of type string and a node field, which returns a Link object.
  • PageInfo: conatins an endCursor field of type string along with a hasNextPage boolean.
  • Response: this is the object type that will be returned by the LinksQuery. This contains the pageInfo object type and edges, which is an arry of type Edge.

Now update the LinksQuery to the following code:

export const LinksQuery = extendType({
type: 'Query',
definition(t) {
t.field('links', {
type: 'Response',
args: {
first: intArg(),
after: stringArg(),
},
async resolve(_, args, ctx) {
return {
edges: [
{
cursor: '',
node: {
title: '',
category: '',
imageUrl: '',
url: '',
description: '',
},
},
],
}
},
})
},
})
Copy

You will need to import intArg() and stringArg() functions from nexus. This is what the what the generated GraphQL schema in the /graphql/schema.graphql file will look like after you save the file and your app is running:

# /graphql/schema.graphql
### This file was generated by Nexus Schema
### Do not make changes to this file directly
type Link {
category: String
description: String
id: String
imageUrl: String
index: Int
title: String
url: String
userId: Int
}
type Response {
edges: [Edges]
pageInfo: PageInfo
}
type Edges {
cursor: String
node: Link
}
type PageInfo {
endCursor: String
hasNextPage: Boolean
}
type Query {
links(after: String, first: Int): Response
}
### rest of the schema down below

Updating the resolver to return paginated data from the database

To use cursor-based pagination with Prisma Client, you need to pass an id to the cursor field.

const results = await prisma.link.findMany({
take: 4,
skip: 1,
cursor: {
id: myCursor,
},
})

Now update the LinksQuery in /graphql/types/Link.ts to the following code:

// /graphql/types/Link.ts
// get ALl Links
export const LinksQuery = extendType({
type: 'Query',
definition(t) {
t.field('links', {
type: 'Response',
args: {
first: intArg(),
after: stringArg(),
},
async resolve(_, args, ctx) {
let queryResults = null
if (args.after) {
// check if there is a cursor as the argument
queryResults = await ctx.prisma.link.findMany({
take: args.first, // the number of items to return from the database
skip: 1, // skip the cursor
cursor: {
id: args.after, // the cursor
},
})
} else {
// if no cursor, this means that this is the first request
// and we will return the first items in the database
queryResults = await ctx.prisma.link.findMany({
take: args.first,
})
}
// if the initial request returns links
if (queryResults.length > 0) {
// get last element in previous result set
const lastLinkInResults = queryResults[queryResults.length - 1]
// cursor we'll return in subsequent requests
const myCursor = lastLinkInResults.id
// query after the cursor to check if we have nextPage
const secondQueryResults = await ctx.prisma.link.findMany({
take: args.first,
cursor: {
id: myCursor,
},
orderBy: {
index: 'asc',
},
})
// return response
const result = {
pageInfo: {
endCursor: myCursor,
hasNextPage: secondQueryResults.length >= args.first, //if the number of items requested is greater than the response of the second query, we have another page
},
edges: queryResults.map(link => ({
cursor: link.id,
node: link,
})),
}
return result
}
//
return {
pageInfo: {
endCursor: null,
hasNextPage: false,
},
edges: [],
}
},
})
},
})
Copy

The first thing this resolver does is check if a cursor is passed as an argument to the query from the client. If there is no cursor, then this is the initial request.

If this initial request returns an empty array, a Response object is returned that indicates that there are no links in the database.

If the initial request returns links, you get the last element's id from the initial result set and pass it in a second query as the cursor.

You then return a Response object, which includes the cursor. That cursor can then be passed as an argument to the query from the client.

Here is a diagram that summarizes how pagination works on the server:

How Pagination works on the serverHow Pagination works on the server

Pagination on the client using fetchMore()

Now that the API supports pagination, you can fetch paginated data on the client using Apollo Client.

The useQuery hook returns an object containing data, loading and errors. However, useQuery also returns a fetchMore() function, which is used to handle pagination and updating the UI when a result is returned.

Navigate to the /pages/index.tsx file and update it to use the following code to add support for pagination:

// /pages/index.tsx
import Head from "next/head";
import { gql, useQuery, useMutation } from "@apollo/client";
import Link from "next/link";
import { AwesomeLink } from "components/AwesomeLink";
const AllLinksQuery = gql`
query allLinksQuery($first: Int, $after: String) {
links(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
edges {
cursor
node {
index
imageUrl
url
title
category
description
id
}
}
}
}
`;
function Home() {
const { data, loading, error, fetchMore } = useQuery(AllLinksQuery, {
variables: { first: 2 },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
const { endCursor, hasNextPage } = data.links.pageInfo;
return (
<div>
<Head>
<title>Awesome Links</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="container mx-auto max-w-5xl my-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{data?.links.edges.map(({ node }) => (
<AwesomeLink
title={node.title}
category={node.category}
url={node.url}
id={node.id}
description={node.description}
imageUrl={node.imageUrl}
/>
))}
</div>
{hasNextPage ? (
<button
className="px-4 py-2 bg-blue-500 text-white rounded my-10"
onClick={() => {
fetchMore({
variables: { after: endCursor },
updateQuery: (prevResult, { fetchMoreResult }) => {
fetchMoreResult.links.edges = [
...prevResult.links.edges,
...fetchMoreResult.links.edges,
];
return fetchMoreResult;
},
});
}}
>
more
</button>
) : (
<p className="my-10 text-center font-medium">
You've reached the end!{" "}
</p>
)}
</div>
</div>
);
}
export default Home;
Copy

You are first passing a variables object to the useQuery hook, which contains a key called first with a value of 2. This means you will be fetching two links. You can set this value to any number you want.

The data variable will contain the data returned from the intial request to the API.

You are then destructuring the endCursor and hasNextPage values from the pageInfo object.

If hasNextPage is true, we will show a button that has an onClick handler. This handler returns a function that calls the fetchMore() function, which receives an object with the following fields:

  • Avariables object that takes the endCursor returned from the intial data.
  • updateQuery function, which is responsible for updating the UI by combining the previous results with the results returned from the second query.

If hasNextPage is false, it means there are no more links that can be fetched.

If you save and your app is running, you should be able to fetch paginated data from your database.

Summary and Next-steps

Congratulations! You successfully completed the second part of the course! If you run into any issues or have any questions, feel free to reach out in our Slack community.

In this part, you learned about:

  • The advantages of using GraphQL over REST
  • How to build a GraphQL API using SDL
  • How to build a GraphQL API using Nexus and the benefits it offers
  • How to add suport for pagination in your API and how to send paginated query from the client

In the next part of the course, you will:

  • Add authentication using Auth0 to secure the API endpoint, this way, only logged in users can view the links
  • Create a mutation so that a logged-in user can bookmark a link
  • Create an admin-only route for creating links
  • Set up AWS S3 to handle file uploads
  • Add a mutation to create links as an admin

Join the discussion

Follow @prisma on Twitter

Don’t miss the next post!

Sign up for the Prisma newsletter