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
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;
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`,
});
},
});
... and export it in server/nexus/_types.ts
:
export * from "./hello";
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
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);
});
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 };
}
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,
});
// ...
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;
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
We first need to add a nexus-prisma
generator block in prisma/schema.prisma
:
generator nexusPrisma {
provider = "nexus-prisma"
}
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;
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);
},
});
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)
Just an fyi the nexus update makes it that you don’t need to cast unknown graphql for graphql version 16.
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.
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
Is this available yet? :D
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).
Thank you will do, your post is really great and helps me alot with understanding and getting started. Appriciate it.