Prisma Nexus for GraphQL in Node.js
A step-by-step guide to integrating Prisma Nexus into a Node.js app for building type-safe, database-backed GraphQL APIs with ease

My first GraphQL project was a disaster. Not because the queries were wrong — they weren't. The problem was that I had the same "User" type defined in four separate places: the SDL file, a TypeScript interface, a Prisma model, and a Zod validator a teammate added three months after launch. Every time something changed, at least one of those four drifted. We'd ship a fix and discover the TypeScript types still thought phone was required even though we'd removed it from the database six weeks ago.
That's the problem Prisma + Nexus was built to eliminate. Nexus generates your GraphQL schema and TypeScript types from the same place you define your Prisma data model. One source. Everything else is derived. You change the definition once and the types, schema, and resolver signatures all update together. It sounds obvious when you read it back. Living without it is what teaches you how much that gap costs.
This walkthrough builds a Node.js GraphQL API from scratch — seven steps, all the code, no gaps. You'll end up with a working server connected to PostgreSQL that you can actually extend and reason about. Solid enough for a production ecommerce backend, not just a tutorial that falls apart the moment you add a second model.
Table of Contents
What to Have Ready Before Step 1
No preamble. Three things:
Node.js installed. Grab the latest LTS from nodejs.org if not.
Prisma CLI globally available:
npm install -g prisma
- A PostgreSQL database running somewhere you can connect to. Local Docker container, Supabase free tier, Railway — doesn't matter as long as you have a connection string ready.
Quick note for anyone dropping this onto a codebase that already exists: Prisma will try to reconcile your schema.prisma with whatever's already in the database during the first migration. On a messy legacy schema, that reconciliation produces a big, scary-looking diff. Read it before you run it. Dev environment first, always. Starting a project from scratch? Ignore all of that — you won't hit it.
Step 1: Get the Project Running
Shortest step in the guide. Make a folder, install every dependency at once:
mkdir prisma-nexus-graphql
cd prisma-nexus-graphql
# Initialize your project
npm init -y
# Install required dependencies
npm install graphql nexus prisma express apollo-server-express path
All seven packages in one shot. The GraphQL runtime, Nexus for code-first schema building, Prisma, and the Apollo + Express combo for the server. Installing together isn't just tidier — npm resolves peer dependencies across the full set at once, rather than grabbing potentially incompatible minor versions when you install things one at a time over the course of a tutorial.
Step 2: Connect Prisma to Your Database
npx prisma init
Follow the prompts, pick PostgreSQL. When the command finishes you'll have two new things:
prisma/schema.prisma— your data model goes here.env— yourDATABASE_URLconnection string goes here, right now, before you do anything else
Seriously. Before the schema. Before the first migration. Before you open another file. Paste the connection string into .env now, because every Prisma command from this point forward tries to connect to the database — and the errors when the string is wrong or blank are genuinely misleading. They won't say "invalid connection string." They'll say something about the client not being initialized, and you'll spend fifteen minutes chasing the wrong problem.
Step 3: Write the Prisma Schema — This Is Not Your GraphQL Schema
If you've built GraphQL APIs without Prisma before, resist the urge to treat schema.prisma as the place to design your API. It isn't. It describes what lives in your database — tables, columns, relationships, constraints. The API shape gets derived from this later, through Nexus. Keep those two concerns separated in your head and the mental model holds together cleanly.
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
}
Once the model is written, run the migration:
npx prisma migrate dev
Two things happen here that nothing else in the setup does: your actual database table gets created, and Prisma Client gets generated with TypeScript types that match your current schema exactly. Skip this and the Client won't know a User model exists. The errors are type-level errors in generated code — files you don't own, call stacks that dead-end in Prisma internals. There's no debugging shortcut around it. Run the migration, and run it again every single time the schema changes.
Step 4: Nexus — Why One More File Is Worth It
Most people hit this step and wonder whether Nexus is actually necessary. You can absolutely build a GraphQL server without it — write the SDL by hand, define TypeScript interfaces manually, wire up resolvers. Plenty of projects do. They also tend to grow a specific class of bug where the SDL says one thing, the TypeScript says another, and the resolver returns a third, and working out which of the three is the authoritative version takes longer than writing any of them originally did.
Nexus makes SDL a generated artifact rather than a source file. You write your types in TypeScript, Nexus generates the SDL and the matching type definitions, and the three things that used to drift are now one thing that structurally can't. Here's schema.ts:
// schema.ts
import { makeSchema } from 'nexus';
import path from 'path';
import * as resolvers from './resolvers';
const schema = makeSchema({
types: [resolvers],
outputs: {
schema: path.join(__dirname, './generated/schema.graphql'),
typegen: path.join(__dirname, './generated/nexus.ts'),
},
});
export default schema;
The outputs block tells Nexus where to write its artifacts. generated/schema.graphql gets the SDL. generated/nexus.ts gets the TypeScript definitions. Both files regenerate on every run. You never edit them. If you open generated/nexus.ts and see something worth changing — stop, find the source definition, change it there, let Nexus regenerate. Editing the generated output is like editing a compiled binary: technically possible, immediately overwritten.
Step 5: Resolvers — Connecting the Schema to the Database
// resolvers.ts
import { extendType, stringArg, nonNull, objectType } from 'nexus';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const User = objectType({
name: 'User',
definition(t) {
t.nonNull.id('id')
t.string('name')
t.string('email')
},
})
export const Query = extendType({
type: 'Query',
definition(t) {
t.list.field('users', {
type: 'User',
resolve: async () => {
return await prisma.user.findMany();
},
});
},
});
export const Mutation = extendType({
type: 'Mutation',
definition(t) {
t.field('createUser', {
type: 'User',
args: {
name: nonNull(stringArg()),
email: nonNull(stringArg()),
},
resolve: async (_, args) => {
return await prisma.user.create({
data: {
name: args.name,
email: args.email,
},
});
},
});
},
});
The PrismaClient instantiation at the top of this file — outside any function, at module scope — is a decision that matters more than it looks.
Every call to new PrismaClient() opens a new connection to the database. If that call lives inside a resolver, it fires on every request. Under normal dev load of one or two requests a second, PostgreSQL barely notices. Under the kind of concurrent traffic a real ecommerce promotion generates — a few hundred users hitting checkout at the same time — you'll saturate the connection limit and requests will start queueing, then timing out, then failing. Module-level instantiation gives you exactly one connection the entire process shares. Experienced Node.js developers do this automatically. Everyone else learns it from an incident. Now you know before the incident.
Step 6: The Server
// server.ts
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import schema from './schema';
const app = express();
const server = new ApolloServer({ schema });
const startServer = async () => {
await server.start(); // Start Apollo Server
server.applyMiddleware({ app }); // Apply Apollo Server middleware to Express
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}/graphql`);
});
}
startServer().catch((err) => {
console.error('Error starting the server:', err);
});
One thing worth naming before you hit it: await server.start() must appear before server.applyMiddleware(). This wasn't a requirement in Apollo 2. Apollo 3 added an explicit async startup phase, and every piece of sample code written before late 2021 is missing it. The resulting error — Server must be started before calling server.applyMiddleware — is at least descriptive, but knowing why it exists saves you the search. One added line, fully resolved.
Step 7: Start It. Break It. Trust It.
node server.ts
http://localhost:4000/graphql — GraphQL Playground opens. Run the mutation first:
# Fetch Users
query {
users {
id
name
email
}
}
# Create Users
mutation {
createUser(name: "John Doe", email: "john@example.com") {
id
name
email
}
}
Mutation, then query. Watch the created user come back in the response.
Then do something the tutorial didn't ask you to do: open your database client — psql, TablePlus, DBeaver, whatever you keep installed — and look at the User table directly. Your row is there. Written by a GraphQL mutation you defined using Nexus types, executed by Prisma Client, stored in a real PostgreSQL table. The whole chain visible at once. For most developers who've built with REST and raw SQL before, that moment is when the value of this stack stops being something you read about and becomes something you understand.
What You Built and What You Still Need to Add
This is a real backend scaffold, not a demo. The same structure — Prisma model, migration, Nexus objectType, resolvers, Apollo/Express server — repeats for every new model you add. Product, Order, Cart — same steps, same pattern, same guarantees. Add a relation in schema.prisma, run migrate dev, write the resolver. The types update automatically because the tooling enforces it, not because the team has to remember.
What you don't have yet: authentication, authorization, rate limiting, input sanitization. Nexus enforces type correctness. It enforces nothing about who can call what. Right now, createUser takes requests from anyone who can reach port 4000. Fine for local dev. Not fine for anything with a public URL. Auth middleware belongs on the roadmap before this touches a non-local environment.
The Prisma docs cover relations, filtering, and pagination. The Nexus docs cover field authorization and custom scalars. Both are worth reading properly rather than searching reactively when something breaks — which is a rarer property in technical documentation than it ought to be.
Read more — Prisma Nexus for GraphQL in Node.js
Himanshu Pant Chief Operating Officer at Innostax
Innostax is a global software consulting and custom software development company helping growth-stage startups, scaleups, and enterprises build reliable, scalable digital products. Founded in 2014 and headquartered in Framingham, Massachusetts, Innostax specializes in custom software development, web and mobile app development, IT staff augmentation, offshore software development, and digital transformation services — across industries including healthcare, retail, education, travel, and fintech. With a dedicated development team model, a 2-week risk-free trial, and deep expertise in technologies like React.js, Node.js, Python, .NET, and React Native, Innostax co-creates breakthrough solutions that help founders, CTOs, and product leaders ship better software, faster. Learn more at innostax.com.





