DEV Community

Pascal Martineau
Pascal Martineau

Posted on • Updated on

Code-first schema definition

Having the Prisma client ready to work with our data on the server side is great, although we still need to define and expose a GraphQL API for interacting with the front end.

But before setting up the GraphQL API endpoint, it's a good idea to create a basic schema first.

Defining the schema using Nexus types

The code-first approach we propose here is based on Nexus, since it will eventually have first-class integration with Prisma (see here).

Let's add Nexus and GraphQL to our project:

yarn add -D nexus graphql
Enter fullscreen mode Exit fullscreen mode

We can structure the server-side source code to make it somewhat scaleable by having our main schema file in server/schema.ts and all of our Nexus types in nexus/*.ts (we don't use types/ to avoid confusion with TypeScript types).

Also, exporting all of these types from a single source file simplifies the schema construction, and naming it server/nexus/_types.ts will keep this file above the others inside the directory. We could also use server/nexus/**/*.ts and server/nexus/index.ts for larger projects.

With this structure in mind, let's create server/schema.ts:

import { resolve } from "pathe";
import { GraphQLSchema } from "graphql";
import { makeSchema } from "nexus";
import * as types from "./nexus/_types";

export default makeSchema({
  types,
  shouldGenerateArtifacts: process.env.NODE_ENV === "development",
  outputs: {
    schema: resolve(process.cwd(), "generated/schema.graphql"),
    typegen: resolve(process.cwd(), "generated/nexus-types.ts"),
  },
}) as unknown as GraphQLSchema;
Enter fullscreen mode Exit fullscreen mode

We need to cast the schema using as unknown as GraphQLSchema until Nexus properly supports GraphQL 16, see this issue for details.

We set shouldGenerateArtifacts and outputs so that Nexus will generate the exposed GraphQL schema and its corresponding typings in the generated/ directory (in development mode only). These are useful for type safety and generating further artifacts with graphql-codegen (see the next article of this series).

Since we only want a minimal schema at this point, let's create a simple hello query in server/nexus/hello.ts (see the Nexus documentation to learn more):

import { extendType } from "nexus";

export const HelloQuery = extendType({
  type: "Query",
  definition(t) {
    t.nonNull.field("hello", {
      type: "String",
      resolve: () => `Hello Nexus`,
    });
  },
});
Enter fullscreen mode Exit fullscreen mode

... and export it in server/nexus/_types.ts:

export * from "./hello";
Enter fullscreen mode Exit fullscreen mode

Our schema is now ready to be exposed via a GraphQL API.

Exposing the GraphQL API endpoint

While there are many GraphQL server packages available, we need one that will play nicely with Nuxt3's server engine (Nitro / h3). In the spirit of keeping things extensible and framework-agnostic, GraphQL Helix seems like a really good choice. Let's add it to our project:

yarn add -D graphql-helix
Enter fullscreen mode Exit fullscreen mode

With Nuxt3's auto-discovery of API endpoints, we can setup the /api/graphql endpoint by simply creating the server/api/graphql.ts file, and using GraphQL Helix to process the incoming request in the handler function:

import { defineHandle, useBody, useQuery } from "h3";
import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from "graphql-helix";
import schema from "../schema";

export default defineHandle(async (req, res) => {
  // Construct GraphQL request
  const request = {
    body: req.method !== "GET" && (await useBody(req)),
    headers: req.headers,
    method: req.method || "GET",
    query: useQuery(req),
  };

  // Render GraphiQL in development only
  if (process.env.NODE_ENV === "development" && shouldRenderGraphiQL(request))
    return renderGraphiQL({ endpoint: "/api/graphql" });

  // Process GraphQL request and send result
  const { operationName, query, variables } = getGraphQLParameters(request);
  const result = await processRequest({
    operationName,
    query,
    variables,
    request,
    schema,
  });
  sendResult(result, res);
});
Enter fullscreen mode Exit fullscreen mode

We now have a GraphQL over HTTP specification-compliant server exposing the schema we built with Nexus types.

You may have noticed from the code above that we also get a configured GraphiQL instance at http://localhost:3000/api/graphql in development mode, nice!

Providing the GraphQL execution context

To be useful, our resolvers need access to the Prisma client instance and eventually to the currently logged-in user. These are usually passed to the resolvers in the GraphQL execution context. Fortunately, GraphQL Helix provides an easy way to provide this with full type safety.

First, we'll define our Context type and its corresponding context factory function inside server/context.ts:

import { prisma } from "../prisma/client";

export type Context = {
  prisma: typeof prisma;
};

export async function contextFactory(): Promise<Context> {
  return { prisma };
}
Enter fullscreen mode Exit fullscreen mode

Then, back in our handler function inside server/api/graphql.ts, we'll pass contextFactory to processRequest and specify Context as its type variable:

import type { Context } from "../context";
import { contextFactory } from "../context";

// ...
  const result = await processRequest<Context>({
    // ...
    contextFactory,
  });
// ...
Enter fullscreen mode Exit fullscreen mode

Finally, we have to configure Nexus to use our Context type in server/schema.ts by setting the contextType option in makeSchema like so:

export default makeSchema({
  // ...
  contextType: {
    module: resolve(process.cwd(), "server/context.ts"),
    export: "Context",
  },
}) as unknown as GraphQLSchema;
Enter fullscreen mode Exit fullscreen mode

After running yarn dev to update generated/nexus-types.ts, our resolvers should have access to the fully-typed execution context.

Projecting the Prisma schema

Defining the exposed GraphQL schema will often involve re-creating our Prisma models using Nexus types, which can be tedious and error-prone. By using the official Prisma plugin for Nexus, we can easily project models from the data layer unto GraphQL types in our API layer.

Please note that nexus-prisma is still in early preview, so it should not be used in production right now.

Now that you've been warned, let's add it to our project:

yarn add -D nexus-prisma
Enter fullscreen mode Exit fullscreen mode

We first need to add a nexus-prisma generator block in prisma/schema.prisma:

generator nexusPrisma {
  provider = "nexus-prisma"
}
Enter fullscreen mode Exit fullscreen mode

This will generate the available Nexus types directly in node_modules/nexus-prisma/ on the next prisma generate. Similarly to the Prisma client, this package does not work well with ES modules (see this issue), so until it does we have to re-export all of our models and enums in prisma/nexus.ts:

import NP, * as NPS from "nexus-prisma/dist-cjs/entrypoints/main";

export const User = NP?.User || NPS?.User;
export const UserRole = NP?.UserRole || NPS?.UserRole;
Enter fullscreen mode Exit fullscreen mode

To project the User and UserRole in our API layer, we add the following in server/nexus/user.ts (remember to export it from server/nexus/_types.ts):

import { enumType, objectType } from "nexus";
import { User, UserRole } from "../../prisma/nexus";

export const UserRoleEnum = enumType(UserRole);

export const UserObject = objectType({
  name: User.$name,
  description: User.$description,
  definition(t) {
    t.field(User.id);
    t.field(User.email);
    t.field(User.role);
  },
});
Enter fullscreen mode Exit fullscreen mode

You'll notice in the code above that we omitted the password field so it can never be returned by our API.

While this example only covers the basic features of nexus-prisma, feel free to read its documentation to learn more.

Top comments (6)

Collapse
 
frosty21 profile image
Nathan Froese

Just an fyi the nexus update makes it that you don’t need to cast unknown graphql for graphql version 16.

Collapse
 
lewebsimple profile image
Pascal Martineau

Thanks for the precision!
I've abandoned Nexus in favor of Pothos GraphQL ... I'll update this article or create a new one in the coming weeks / months.

Collapse
 
frosty21 profile image
Nathan Froese

Thats fair. IMHO Nexus documentation is a little bit of spaghetti documentation atm. I would go Pothos Graphql too looking forward to your next article

Collapse
 
indiehjaerta profile image
Mikael

Is this available yet? :D

Thread Thread
 
lewebsimple profile image
Pascal Martineau

Slowly getting there, probably around xmas. In the meantime, you can check out my upcoming Nuxtastic template (which also needs an update to the latest libraries).

Thread Thread
 
indiehjaerta profile image
Mikael

Thank you will do, your post is really great and helps me alot with understanding and getting started. Appriciate it.