Building a Scalable GraphQL Server with Next.js
Setting Up Next.js
To get started, run the following command to create a new Next.js application:
yarn create next-app
Then, modify the tsconfig.json
file to enforce stricter TypeScript rules.
You can find the full list of packages used in this article in the package.json
file.
Configuring Prisma
Prisma will be our tool of choice for loading data from our Postgres database. Run the following command to generate a Prisma folder containing a schema.prisma
file:
npx prisma init
Update the schema.prisma
file to include two models: Album
and Artist
. Then, create and run a database migration using the following command:
npx prisma migrate dev --name init
Loading Seed Data
When developing locally, it’s essential to have consistent seed data. Update the scripts
section in your package.json
file to include a ts-node
script, required to execute the Prisma seed command.
"scripts": {
"seed": "ts-node prisma/seed"
}
Create a seed.ts
file in the Prisma folder to generate data for local development. Finally, run the following command to populate your local database:
npx prisma db seed --preview-feature
Adding an API Route in GraphQL
Create a file called graphql.ts
within the pages/api
folder. For now, its contents will simply respond with the text “GraphQL!”. But with this setup, you could respond with any JSON data, reading query params, headers, etc. from the req
object.
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.status(200).json({ message: 'GraphQL!' });
}
GraphQL Context
Every resolver in GraphQL receives a context, which is a place to put global things like an authenticated user, database connection (Prisma), and DataLoader (to avoid N+1 queries). We’ll start by exporting a context function that returns an instance of the Prisma client.
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function context(): Promise<{ prisma: typeof prisma }> {
return { prisma };
}
Setting Up GraphQL Schema
With our context in place, it’s time to generate our GraphQL schema using Nexus. We’ll create three GraphQL types: Query
, Artist
, and Album
.
import { makeSchema } from 'nexus';
import { context } from './context';
export const schema = makeSchema({
types: [Query, Artist, Album],
outputs: {
schema: __dirname + '/../schema.graphql',
typegen: __dirname + '/../typegen.ts',
},
sourceTypes: {
modules: [
{
module: 'prisma/generated/prisma-client/index.d.ts',
alias: 'prisma',
},
],
},
contextType: 'Context',
nonNullDefaults: {
input: false,
output: false,
},
});
Top-Level Query Type
The Query
type is one of the top-level fields of your GraphQL API, along with Mutation
and Subscription
. We’ll define one field: albums
, which will use the Prisma client from our context to load the albums.
import { objectType } from 'nexus';
import { prisma } from '../context';
export const Query = objectType({
name: 'Query',
definition(t) {
t.list.field('albums', {
type: 'Album',
resolve: async (_, __, ctx) => {
return ctx.prisma.album.findMany();
},
});
},
});
Avoiding N+1 Queries with DataLoader
To avoid the hidden problem of loading the artist for each album, we’ll define a loader function to pool up IDs (of artists) and load them all at once in a single batch.
import { loader } from 'nexus/loader';
const artistLoader = loader<{ id: number }, Artist>('Artist', {
fetch: async (keys, ctx) => {
const artists = await ctx.prisma.artist.findMany({
where: { id: { in: keys.map(key => key.id) } },
});
return keys.map(key => artists.find(artist => artist.id === key.id));
},
});
This allows us to update our Album
type to utilize the DataLoader inside of the artist resolver, resulting in a single query to the database to load all the artists at once.
import { objectType } from 'nexus';
import { artistLoader } from './artistLoader';
export const Album = objectType({
name: 'Album',
definition(t) {
t.string('name');
t.field('artist', {
type: 'Artist',
resolve: async (parent, _, ctx) => {
return artistLoader.load({ id: parent.artistId });
},
});
},
});
- The final result is a typed, code-first GraphQL server in Next.js, loading data from Postgres with Prisma, and eliminating N+1 performance issues using DataLoader.
- The next step might involve adding mutations along with authentication to your app, enabling users to create and modify data with the correct permissions.