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.

Leave a Reply