Leigh Halliday Leigh Halliday is a developer based out of Canada who works at FlipGive. He writes about React and Ruby on his blog and publishes React tutorials on YouTube.

Building a GraphQL server in Next.js via API routes

6 min read 1881

building graphql API with nextjs feature image

Editor’s Note: This post was updated on 14 May 2021 to reflect updated information and new code using TypeScript to add type safety and with DataLoader to avoid N+1s, using Prisma instead of Knex for the database layer, and incorporating a code-first GraphQL schema approach.

Many people only think of Next.js as a frontend React framework, providing server-side rendering, built-in routing, and a number of performance features. All of this is true, but Next.js also supports serverless functions via its API routes, which is an easy way to provide a backend to your React frontend code, all within the same application.

In this article, we will learn how to use API routes to set up a GraphQL API within Next.js. We will work with a fully typed setup: TypeScript, loading data from Postgres with Prisma, and using a code-first GraphQL Schema with Nexus.

We will also learn how to improve performance using the DataLoader package and pattern, avoiding costly N+1 queries.Full source code can be found here.

Building the GraphQL server

At the end of this article, we want to be able to perform the following GraphQL query of albums and artists, loaded efficiently (with only two SQL queries) from our Postgres database:

{
  albums(first: 5) {
    id
    name
    year
    artist {
      id
      name
    }
  }
}

Producing output which might resemble:

{
  "data": {
    "albums": [
      {
        "id": 3,
        "name": "Rotation & Frequency",
        "year": "2020",
        "artist": {
          "id": 2,
          "name": "Slick Shoes"
        }
      },
      {
        "id": 5,
        "name": "I Am",
        "year": "2018",
        "artist": {
          "id": 3,
          "name": "Sleeping Giant"
        }
      }
      // etc...
    ]
  }
}

Setting up Next.js

The easiest way to set up Next.js is to run the command yarn create next-app. As soon as you change one of the files in Next.js to a TypeScript extension (.ts or .tsx) and run yarn dev, it will ask you to install a few additional packages and will create a tsconfig.json file automatically.

Feel free to avoid this step, but we’re going to modify the tsconfig.json file slightly to make TypeScript enforce stricter rules:

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "lib": ["dom", "esnext"],
    "incremental": true,
    "allowJs": false,
    "checkJs": false,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "jsx": "preserve",
    "allowSyntheticDefaultImports": true,

    /* Module Resolution */
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,

    /* Type Checking */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules", ".next"]
}

For a full list of the packages used in this article, please refer to the package.json file.

Configuring Prisma

Prisma will be the tool we use to load data from our Postgres database. To get started, run the command npx prisma init, which will generate a prisma folder that contains a schema.prisma file.

Note that it references a DATABASE_URL environment variable. Ensure that this is available in via a .env file (but don’t commit it!). We will modify the schema file to include two models: Album and Artist.

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

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

model Album {
  id       Int    @id @default(autoincrement())
  name     String
  year     String
  artist   Artist @relation(fields: [artistId], references: [id])
  artistId Int
}

model Artist {
  id     Int     @id @default(autoincrement())
  name   String
  url    String
  albums Album[]
}

In order to create the Album and Artist tables in our database, we’ll need to tell Prisma to create and run a database migration:npx prisma migrate dev --name init.

Loading seed data with Prism

When developing locally, it’s good to have some consistent seed data to work with. Prisma supports database seeding, but first let’s update the scripts section in our package.json file to have a ts-node script, required to execute the Prisma seed command.

// package.json
{
  "scripts": {
    "ts-node": "ts-node --compiler-options '{\"module\":\"CommonJS\"}'"
  }
}

Next we’ll create a seed.ts file in the prisma folder. Its purpose is to generate any data you’ll want to use locally when developing. Feel free to put your favourite bands in here!

// prisma/seed.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

async function main() {
  const comeback = await prisma.artist.create({
    data: {
      name: "Comeback Kid",
      url: "https://comeback-kid.com/",
      albums: {
        create: [
          { name: "Turn It Around", year: "2003" },
          { name: "Wake the Dead", year: "2005" },
        ],
      },
    },
  });
  console.log({ comeback });
}

main()
  .catch((e) => {
    console.error(e);
    process.exit(1);
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

Once you’re ready, run npx prisma db seed --preview-feature to execute the seed file and populate our local database.

Adding an API route in GraphQL

With Next.js set up, we’re going to add an API (server) route to our app. This is as easy as creating a file within the pages/api folder called graphql.ts. For now, its contents will be:

// pages/api/graphql.ts
export default (_req, res) => res.end("GraphQL!");

Done! Isn’t GraphQL easy? Just joking… The above code will simply respond with the text “GraphQL!”, but with this setup we could respond with any JSON that we wanted, reading query params, headers, etc. from the req (request) object.

Time for the actual GraphQL handling. For this, we’ll use ApolloServer (from the apollo-server-micro package — this one works great in Next.js). You’ll notice that we import schema and context from two files. These don’t exist yet but are where we’ll spend the bulk of our time later on.

// pages/api/graphql.ts
import { ApolloServer } from "apollo-server-micro";
import { schema } from "../../src/schema";
import { context } from "../../src/context";

const server = new ApolloServer({ schema, context });
const handler = server.createHandler({ path: "/api/graphql" });

export default handler;

GraphQL context

Every resolver in GraphQL receives something called the context. It is a place to put global things such as:

  • Authenticated user
  • Database connection (Prisma)
  • DataLoader (to avoid N+1 queries)

We don’t have authenticated users in this example, but we do have the other two. We’ll start by exporting a context function that returns an instance of the Prisma client.

// src/context.ts
import { PrismaClient } from "@prisma/client";
import { Artist } from ".prisma/client";

const prisma = new PrismaClient({ log: ["query"] });

export interface Context {
  prisma: PrismaClient;
}

export function context(): Context {
  return { prisma };
}

Setting up GraphQL schema

With our context in place, it’s time to generate our GraphQL schema. Often you will find handwritten schema.graphql files, along with corresponding resolvers to load the data to be returned from each field.



This is not the case with Nexus. Nexus takes a code-first approach where the schema.graphql file is a generated artifact from the GraphQL type definition. This means you don’t have to keep them in sync!

We will create three GraphQL types: Query, Artist, and Album. The key sections of this makeSchema call are:

  • outputs: Where do you want the schema.graphql and TypeScript types output to? These are the artifacts I mentioned above
  • sourceTypes: If you have pre-existing TypeScript types that align with your GraphQL types, you can avoid recreating them! We’ll be pointing it to the types created from our Prisma Client
  • contextType: This allows us to tell Nexus where to look for the TypeScript type of our context
  • nonNullDefaults: We are going to define each of our GraphQL fields as non-null by default… this can be overridden but I feel like it’s better to start with strict code by default
// src/schema.ts
import { makeSchema, objectType, queryType } from "nexus";
import { join } from "path";

export const schema = makeSchema({
  types: [Query, Artist, Album],
  shouldGenerateArtifacts: process.env.NODE_ENV === "development",
  outputs: {
    schema: join(process.cwd(), "schema.graphql"),
    typegen: join(process.cwd(), "nexus.ts"),
  },
  sourceTypes: {
    modules: [{ module: ".prisma/client", alias: "prisma" }],
    debug: process.env.NODE_ENV === "development",
  },
  contextType: {
    module: join(process.cwd(), "src", "context.ts"),
    export: "Context",
  },
  nonNullDefaults: {
    input: true,
    output: true,
  },
});

Top-level query type in GraphQL API

The Query type (along with Mutation and Subscription, which we won’t be covering here) are the top level fields of your GraphQL API. They are the entry points into your API. On our Query type, we will define one field: albums, which will use the Prisma client from our context to load the albums.

// src/schema.ts
const Query = queryType({
  definition(t) {
    t.list.field("albums", {
      type: "Album",
      args: {
        first: "Int",
      },
      resolve(_root, args, ctx) {
        return ctx.prisma.album.findMany({ take: args.first });
      },
    });
  },
});

Album and Artist Types

The types for Artist and Album are created using the objectType function from Nexus. Most of the fields can be left without explicit resolvers because they are simply accessing attributes without any manipulation.

The Album type has the ability to load an Artist though, and we’ll create a custom resolver to load the artist from the database.

// src/schema.ts
const Artist = objectType({
  name: "Artist",
  definition(t) {
    t.int("id");
    t.string("name");
    t.string("url");
  },
});

const Album = objectType({
  name: "Album",
  definition(t) {
    t.int("id");
    t.string("name");
    t.string("year");
    t.field("artist", {
      type: "Artist",
      async resolve(album, _args, ctx) {
        const artist = await ctx.prisma.artist.findFirst({
          where: { id: album.artistId },
        });
        // The ! tells TypeScript to trust us, it won't be null
        return artist!;
      },
    });
  },
});

Avoiding N+1 queries with DataLoader

There is a hidden problem with the above resolvers. Specifically, loading the artist for each album. An SQL query gets run for each album, meaning that if you have 50 albums to display, you will have to perform 50 additional SQL queries to load each album’s artist. Marc-André Giroux has a great article on this problem, and we’re going to discover how to solve it right now!

The first step is to define a loader function. The purpose of a loader is to pool up IDs (of artists in our case) and load them all at once in a single batch, rather than each one on its own. You’re lazy-loading the data.

// src/context.ts
const createArtistLoader = () =>
  new DataLoader<number, Artist | null>(async (ids) => {
    // Load all of the artists for the given `ids`
    const artists = await prisma.artist.findMany({
      where: { id: { in: [...ids] } },
    });

    // To make for more efficient lookup, convert to a Map of id => Artist records
    const artistMap = artists.reduce(
      (acc, artist) => acc.set(artist.id, artist),
      new Map<number, Artist>()
    );

    // Map the ids back to the loaded Artist record
    return ids.map((id) => artistMap.get(id) ?? null);
  });

We’ll need to add this DataLoader instance to our Context with the following changes:

// src/context.ts
export interface Context {
  prisma: PrismaClient;
  artistLoader: ReturnType;
}

export function createContext(): Context {
  return { prisma, artistLoader: createArtistLoader() };
}

This allows us to update our Album type to utilize the DataLoader inside of the artist resolver:

const Album = objectType({
  name: "Album",
  definition(t) {
    t.int("id");
    t.string("name");
    t.string("year");
    t.field("artist", {
      type: "Artist",
      async resolve(album, _args, ctx) {
        const artist = await ctx.artistLoader.load(album.artistId);
        return artist!;
      },
    });
  },
});

The end result is a single query to the database to load all the artists at once… N+1 problem solved! We should now see two queries total: One to load the albums, and another to load all of the artists.

Conclusion

In this article we were able to create a typed, code-first GraphQL server in Next.js, loading data from Postgres with Prisma, and stomping out N+1 performance issues using DataLoader. Not bad for a day’s work!

The next step might involve adding mutations along with authentication to our app, enabling users to create and modify data with the correct permissions. Next.js is no longer just for the frontend as you can see. It has first-class support for serverless endpoints and the perfect place to put your GraphQL API.

Monitor failed and slow GraphQL requests in production

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Leigh Halliday Leigh Halliday is a developer based out of Canada who works at FlipGive. He writes about React and Ruby on his blog and publishes React tutorials on YouTube.

Leave a Reply