Building Typesafe Full Stack App w/ Apollo Server 4, Railway, Prisma, Pothos, Next, & TS (part 1 - setting up the server)
Introduction
I've spent the past few days or so setting this up and wanted to share everything I've learned to hopefully help some people out!
This article assumes basic proficiency with Graphql. We're going to be talking about resolvers, schemas, mutations, and queries. If those things sound confusing to you check out Prisma's fresher on GraphQL (opens in a new tab).
Let's get started!
Setting up Next
npx create-next-app@latest --typescript
It will then prompt you for the project name and if you want eslint enabled then begins downloading the packages!
Setting up Postgres (Railway)
It's insane how quickly you can provision a Postgres server on Railway I promise it will blow your mind.
Head over to https://railway.app/new (opens in a new tab) and select "Provision PostgreSQL."
Once the database is provisioned (it'll show the date created in the node box) click on it and go over to the "Connect" tab.
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673231139340/205f444b-bc08-4c3a-b1b8-da46ae879997.png (opens in a new tab) align="center")
From there copy the Postgres Connection URL and paste it into the .env file for the value for the DATABASE_URL key.
BOOM! 🤯 How easy was that? Now let's set up Prisma.
Setting up Prisma
Prisma is a sweet ORM that gives fantastic type safety by auto-generating types from our database schema! This is perfect for our TS project!
To get started:
yarn add -D prisma @prisma/client
After that completes initialize Prisma
npx prisma init
Add the following to the schema.prisma
file
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
hashedPassword String
name String
birthday DateTime
createdAt DateTime @default(now())
isOnline Boolean @default(false)
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
url String
isPublished Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
In summary, we've defined a one-to-many relationship between the User model and Post model. In the Post model we've set a foreign key - "authorId" - paired to a primary key in the User model - "id".
Seeding Database
Now let's seed our database. Create a file seed.ts
in the prisma
folder
import { PrismaClient } from '@prisma/client';
const db = new PrismaClient();
async function seed() {
await Promise.all(
createUsers().map((user) => {
return db.user.create({ data: user });
})
);
}
seed();
function createUsers() {
return [
{
email: 'adam@gmail.com',
name: 'Adam',
username: 'adamIsAwesome',
hashedPassword: '123456',
birthday: new Date('1945-01-01'),
},
{
email: 'bob@gmail.com',
name: 'Bob',
username: 'bob',
hashedPassword: 'asoidf',
birthday: new Date('1965-01-01'),
},
{
email: 'charlie@gmail.com',
name: 'charlie',
username: 'charlie',
hashedPassword: 'pjioas',
birthday: new Date('1985-01-01'),
},
{
email: 'dan@abc.com',
name: 'Dan',
username: 'DanTheMan',
hashedPassword: '1234',
birthday: new Date('1996-07-01'),
},
];
}
Let's sync our database with our Prisma schema:
yarn prisma db push
Let's seed the database. First, we need a way to compile this seed.ts
file. We can run the typescript compiler using tsc
and then point Prisma to that compiled file location or we can use ts-node
I like using ts-node so I'll use that
yarn add -D ts-node
Now let's add the command to the package.json
{
"name": "apollo-prisma-pothos-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
"dependencies": {
"@next/font": "13.1.1",
"@prisma/client": "^4.8.1",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"eslint": "8.31.0",
"eslint-config-next": "13.1.1",
"next": "13.1.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.4"
},
"devDependencies": {
"prisma": "^4.8.1"
}
}
Now we can seed the database
yarn prisma db seed
Now we can check out our data!
yarn prisma studio
Go to the URL specified and you should see this
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673134928724/c336f8ed-fdb6-4dcc-8888-c7dd18c6fc97.png (opens in a new tab) align="center")
Export the Prisma Client
We will want to share the Prisma instance with the other parts of the app, so let's make that accessible!
Create a file called index.ts
in the prisma folder with the following contents:
import { PrismaClient } from '@prisma/client';
let db: PrismaClient;
declare global {
var __db: PrismaClient | undefined;
}
// this is needed to stop creating a new instance of Prisma everytime the server restarts
if (process.env.NODE_ENV === 'production') {
db = new PrismaClient();
} else {
if (!global.__db) {
global.__db = new PrismaClient();
}
db = global.__db;
}
export { db };
Awesome! Now we can access the client from other places in our app.
Setting up Apollo Server
Let's add the stuff required for the Apollo Server
yarn add @apollo/server graphql @as-integrations/next
We're using "@as-integrations/next" to have Apollo Server work seamlessly with Next.
The API folder is where all of the server code runs. We're going to set up the GraphQL server to run at the api/graphql
endpoint so make a file called graphql.ts
in the API folder. In that file, we are going to init the Apollo Server with the schema from Pothos. Then we'll hook up the Next / Apollo Server integration package.
We'll define a schema later so don't worry about that for now. It will look something like this:
graphql.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
const server = new ApolloServer({});
export default startServerAndCreateNextHandler(server);
Now we need to pass a resolver and schema to the ApolloServer instance.
We could use SDL to create the schema and manually create resolvers but we lose the typesafety that a tool like Pothos offers.
So for now let's leave the graphql.ts as is - we'll return after we setup Pothos
Setting up Pothos
Real quick: Pothos supports a Prisma plugin that makes things like defining Prisma object types and solving n+1 issues easier and most importantly shares the types Prisma auto generates to our schemas and resolvers.
To start, install the Pothos package, the Prisma plugin, and graphql-scalars which we will use to create a Date scalar:
yarn add @pothos/core @pothos/plugin-prisma graphql-scalars
Integrate Pothos With Prisma
To get Pothos and Prisma to share types we need to add the following to the top of the schema.prisma
file:
generator pothos {
provider = "prisma-pothos-types"
}
Now every time the Prisma types get re-generated it will create the pothos types.
To regenerate the Prisma types and create the pothos types run:
yarn prisma generate
Setup the Schema Builder
Ok, now we're ready to setup our schema through the SchemaBuilder
API!
Let's create a folder called graphql
in the root directory. Add a file, builder.ts
inside of which we will need to import the SchemaBuilder
to build our schema for GraphQL. From there we can hook it up to Prisma and define our custom scalar to support a date scalar.
builder.ts
import SchemaBuilder from '@pothos/core';
import PrismaPlugin from '@pothos/plugin-prisma';
import { DateResolver } from 'graphql-scalars';
// This is the default location for the generator, but this can be
// customized as described above.
// Using a type only import will help avoid issues with undeclared
// exports in esm mode
import type PrismaTypes from '@pothos/plugin-prisma/generated';
import { db as prisma } from '../../prisma/index';
export const builder = new SchemaBuilder<{
// The types used here will be used for the resolvers
Scalars: {
Date: { Input: Date; Output: Date };
};
PrismaTypes: PrismaTypes;
}>({
plugins: [PrismaPlugin],
prisma: {
client: prisma,
},
});
// in GraphQL the Query type and Mutation type can each only be called once. So we call it here and will add fields to them on the go
builder.queryType();
builder.mutationType();
// This is where we've created the new date scalar
builder.addScalarType('Date', DateResolver, {});
Now let's define some custom types and set up the resolvers.
Defining Our First Custom Object Type - User
Create a folder called schema
and in it create a file called user.ts
Now we'll use the SchemaBuilder to generate our custom object. This is where the Prisma plugin will really shine & we'll begin getting some auto-completion.
This user type will be the first of 2 types we will generate using the Pothos builder.
Let's import our builder and use the prismaObject method available that exposes the types Prisma generated. The first argument is the name of this custom type, followed by the fields. The keys in the field can be anything we want but the arguments passed to the exposeString, for example, are literally our Prisma types and has auto-completion.
One quick note: for the birthday field we want to use our custom date scalar and the syntax is just a little different than the rest of them.
So in user.ts
we'll have:
import { db } from '../../prisma/index';
import { builder } from '../builder';
builder.prismaObject('User', {
fields: (t) => ({
id: t.exposeInt('id'),
name: t.exposeString('name'),
email: t.exposeString('email'),
pass: t.exposeString('hashedPassword'),
isOnline: t.exposeBoolean('isOnline'),
username: t.exposeString('username'),
birthday: t.expose('birthday', {
type: 'Date',
}),
}),
});
Add User Field to Query Type and Provide Resolver
Now that we have our custom type for users defined let's add a field to the root query type for it!
I mentioned earlier that the root query type can only be defined once (which we already did) so we will add fields to it by calling the builder and using the queryFields
method. If we wanted to just have one massive root query we could use the builder's queryType
method which would create an instance of a root query for us.
Ok, so now we know that we need to use a queryField. We'll need to provide the field a name, we'll call it retrieveUser, and then connect it to the Prisma type and have the resolver return some useful data. Every query can have n number of arguments so we will require an id to be passed as an argument. We'll take that id and search for a user using Prisma.
So continuing user.ts
:
// Adding this code to user.ts
// Ok, using the queryField like we mentioned
builder.queryFields((t) => ({
// Here we name our field for the root query
retrieveUser: t.prismaField({
// We are using the 'User' model from Prisma types
type: 'User',
// We want the user to pass an argument of the type int
args: {
id: t.arg.int({ required: true }),
},
// Now we can defined what the resolver returns
resolve: (query, root, args, ctx, info) => {
// We find the user with that id using Prisma
return db.user.findUniqueOrThrow({
where: {
id: args.id,
},
});
},
}),
}));
Add Root Mutation & Resolver for User Type
Now let's define a field to the Mutation type and implement a resolver to create a user. Instead of expecting one single arg (id) we will require all the fields that are required in the Prisma User model.
Same syntax as the queryFields
but instead, it will be mutationFields
.
Remember that the args are what the user will pass through GraphQL - which we can call whatever we want! Since the fields are required I'm going to add the syntax to make them required.
Add this to user.ts
builder.mutationFields((t) => ({
createUser: t.prismaField({
type: 'User',
args: {
name: t.arg.string({ required: true }),
email: t.arg.string({ required: true }),
pass: t.arg.string({ required: true }),
username: t.arg.string({ required: true }),
birthday: t.arg({ type: 'Date', required: true }),
},
resolve: (query, root, args, ctx, info) => {
return db.user.create({
data: {
name: args.name,
email: args.email,
hashedPassword: args.pass,
username: args.username,
birthday: args.birthday,
},
});
},
}),
}));
Ok, I'm excited to go try these out in the Apollo Server sandbox but first, we will need the builder to generate this into a schema using the toSchema
method!
So at the end of user.ts
do a named export for that under whatever name you want, I'm just going to call it schema!
export const schema = builder.toSchema();
Import Schema in Apollo Server
Now back in Apollo Server we can import this schema to get this thing running!
graphql.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { schema } from '../../graphql/schema/user';
const server = new ApolloServer({});
export default startServerAndCreateNextHandler(server);
Interact with Apollo Server Playground
Now we can start the server by running
yarn dev
Now visit http://localhost:3000/api/graphql (opens in a new tab) to begin the Apollo Server playground - it should look like this!
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673155160223/e1eb6ce1-7784-4f8d-b04f-993b96bb80d6.png (opens in a new tab) align="center")
Now if you remove that id with the red squiggly line and type ctrl+space (Windows) / command+space (mac) you'll see the one type we've defined on our Query type. Select that and it will autocomplete the query with the required arguments!
Now inside the braces for the retrieveUser type we can extract all of the fields we defined in our User schema. Hit ctrl+space / command+space to select some fields.
At the bottom of the screen in the variables section set the value of "retrieveUserId" to 1.
Now, depending on what fields you've requested, you should see it's the user "Bob"
Mutations
Let's run a mutation now by running createUser field.
Remove all the code and type mutation {}
and inside of the curly braces type command+space and select the createUser field. Select whatever fields you want to return.
Be sure your variables match the the names in the arguments. It should look like this:
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673156168034/91caf47d-9c77-4553-ae19-7636d1d4133c.png (opens in a new tab) align="center")
Now let's check our database and see the new user via Prisma studio!
yarn prisma studio
If everything worked it should be there in the User table!
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1673156282364/97a25043-946a-4dbf-ae6a-80a6f5c49464.png (opens in a new tab) align="center")
Defining Our Second Custom Object Type - Post
Now in the schema
folder let's add post.ts
to create the Post type.
We'll be doing the same thing that we did in user.ts but with different fields to match the Post model.
import { db } from '../../prisma/index';
import { builder } from '../builder';
builder.prismaObject('Post', {
fields: (t) => ({
id: t.exposeInt('id'),
title: t.exposeString('title'),
content: t.exposeString('content'),
createdAt: t.expose('createdAt', {
type: 'Date',
}),
updatedAt: t.expose('updatedAt', {
type: 'Date',
}),
url: t.exposeString('url'),
isPublished: t.exposeBoolean('isPublished'),
author: t.relation('author'),
}),
});
Add Post Fields to Query and Mutation Type
Now let's define some fields on the Query and Mutation Type. It will be extremely similar to what we did back in user.ts
so I won't walk through the steps this time.
post.ts
// Add this to the post.ts file
builder.queryFields((t) => ({
post: t.prismaField({
type: 'Post',
args: {
id: t.arg.int({ required: true }),
},
resolve: async (query, root, args, ctx, info) => {
return db.post.findUniqueOrThrow({
where: {
id: args.id,
},
});
},
}),
}));
builder.mutationFields((t) => ({
createPost: t.prismaField({
type: 'Post',
args: {
title: t.arg.string({ required: true }),
content: t.arg.string({ required: true }),
url: t.arg.string({ required: true }),
isPublished: t.arg.boolean({ required: true }),
authorId: t.arg.int({ required: true }),
},
resolve: async (query, root, args, ctx, info) => {
return db.post.create({
data: {
title: args.title,
content: args.content,
url: args.url,
isPublished: args.isPublished,
authorId: args.authorId,
},
});
},
}),
}));
Generating Schema with SchemaBuilder
For Multiple Files
Now we've got multiple places that are used for defining our schema: user.ts
and post.ts
. The SchemaBuilder needs to import all the different components to generate the schema for us.
To do this let's first: remove the call to generate the schema at the bottom of user.ts
(don't forget to remove the imported builder too.) The line at the bottom was this:
export const schema = builder.toSchema();
Create an index.ts
file inside of the schema
folder and import our two files in the schema folder that represent our custom object types, User and Post, and the fields that we added to the Query and Mutation types with their respected resolvers. We'll also need to import the SchemaBuilder to combine it all.
You should have this in index.ts
:
import { builder } from '../builder';
import './user';
import './post';
export const schema = builder.toSchema();
Now we'll need to go back to the graphql.ts
file to adjust where the schema for ApolloServer lives. Change the path for the import so it looks like this:
import { schema } from '../../graphql/schema/index';
Conclusion
Now we've got our server in a really good place. Feel free to play around in the Apollo Server Playground to create some new posts and view them!
In Part 2 we'll be setting this up to the Next Client and generating the types so we can get typesafety and auto-complete!
If you have any questions / problems def reach out by either commenting or DM'ing me on Twitter @reillyjodonnell (opens in a new tab)
github repo link: https://github.com/reillyjodonnell/apollo-prisma-pothos-next (opens in a new tab)