This was originally posted in my blog on 6th December 2020.
In this post we'll create a GraphQL API to create, view and delete notes that we can deploy to Vercel using Apollo Server, TypeScript and MongoDB.
Initial Setup
First Vercel will need be downloaded, installed and logged into.
yarn global add vercel
vercel login
The nice thing about using Vercel for development is that we don't have to worry about manually configuring TypeScript.
Then create a folder, initialize a project in it and install typescript
as a dev dependency.
mkdir serverless-api-graphql-prisma
cd serverless-api-graphql-prisma
yarn init --yes
yarn add typescript -D
Setup the Database
We will be using Prisma to connect to and work with our database. In this example we'll be using PostgreSQL.
If you need a database without installing one locally, Heroku has free tier.
Once you have the database set up, keep note of its URL.
Setup Prisma
First install the dependencies.
yarn add @prisma/cli ts-node @types/node -D
Then create tsconfig.json
with the following configuration.
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
Next let's create the Prisma schema file by running the prisma init
command.
npx prisma init
This will create the prisma
folder in the project root and in it the schema.prisma
and .env
files.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Replace the placeholder DATABASE_URL
in your .env
file with the URL of your own database.
Remember to not commit the
.env
file. Add it to.gitignore
.
To organize our code, first let's move the .env
to the project root.
Then move the schema.prisma
file to src\db\schema.prisma
.
// src\db\schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
We also need to modify Prisma to read the schema from its new path by adding the path in package.json
.
{
"prisma": {
"schema": "src/db/schema.prisma"
}
}
Now we need to add the model for Notes. All models are "declared" in the schema.prisma
file.
// src\db\schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Note {
id Int @id @default(autoincrement())
date DateTime @default(now())
title String
content String
}
Creating the database tables
We will be using Prisma Migrate to create the database tables using the models in your schema file.
First enter the following command.
npx prisma migrate save --name init --experimental
This will create a new folder called migrations
in src\db
to store your migration history but it won't actually create the table in your database.
Replace
init
with the name of your migration.
To finish creating the table, run the up
command.
npx prisma migrate up --experimental
Install and Generate the Prisma Client
yarn add @prisma/client
npx prisma generate
When you generate the Prisma Client it will create types for all the database models to use in our code. In our case it'll create one for Note.
Setup the GraphQL API
First let's install the dependencies.
yarn add apollo-server-micro graphql graphql-iso-date
yarn add @types/graphql-iso-date -D
Next create the folder api
in the project root and in it create the file graphql.ts
. Vercel will expose the serverless function in the file as the endpoint /api/graphql. The GraphQL function will be created using Apollo Server. Since we're doing a serverless version we'll be using apollo-server-micro.
Then import ApolloServer
from apollo-server-micro
.
// api/graphql.ts
import { ApolloServer } from "apollo-server-micro";
After that import the GraphQL Schemas and Resolvers. We'll create these later.
// api/graphql.ts
import { ApolloServer } from "apollo-server-micro";
import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
Then initialize the Apollo Server and export it.
// api/graphql.ts
import { ApolloServer } from "apollo-server-micro";
import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
});
export default apolloServer.createHandler({ path: "/api/graphql" });
To finish configuring we'll set the introspection
and playground
options.
// api/graphql.ts
import { ApolloServer } from "apollo-server-micro";
import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
playground: true,
introspection: true,
});
export default apolloServer.createHandler({ path: "/api/graphql" });
Setup the GraphQL Schema
In the src
folder create the graphql
folder and in it create the schema
folder.
GraphQL doesn't have a type for date and time so we'll need a schema for those types. Create custom.ts
in the schema
folder.
// src/graphql/schema/custom.ts
import { gql } from "apollo-server-micro";
export default gql`
scalar Date
scalar Time
scalar DateTime
`;
Next we'll setup the schema for Note.
First create the Note schema file, note.ts
in the schema
folder.
// src/graphql/schema/note.ts
import { gql } from "apollo-server-micro";
export default gql``;
Then add the Note type.
// src/graphql/schema/note.ts
import { gql } from "apollo-server-micro";
export default gql`
type Note {
id: ID!
title: String!
content: String!
date: DateTime!
}
`;
After that add two Queries, one to get all Notes and one to get a specific Note.
// src/graphql/schema/note.ts
import { gql } from "apollo-server-micro";
export default gql`
extend type Query {
getAllNotes: [Note!]
getNote(id: ID!): Note
}
type Note {
id: ID!
title: String!
content: String!
date: DateTime!
}
`;
Next add two Mutations to create new Notes and delete existing ones.
// src/graphql/schema/note.ts
import { gql } from "apollo-server-micro";
export default gql`
extend type Query {
getAllNotes: [Note!]
getNote(id: ID!): Note
}
extend type Mutation {
saveNote(title: String!, content: String!): Note!
deleteNote(id: ID!): Note
}
type Note {
id: ID!
title: String!
content: String!
date: DateTime!
}
`;
Note that we add the keyword
extend
to the Query and Mutation type in the two schemas. This is because we'll be joining all the schemas into one later.
Finally we'll need a schema a join all the others schemas together. Create index.ts
in the schema
folder and declare and export the Link Schema in an array.
// src/graphql/schema/index.ts
import { gql } from "apollo-server-micro";
const linkSchema = gql`
type Query {
_: Boolean
}
type Mutation {
_: Boolean
}
type Subscription {
_: Boolean
}
`;
export default [linkSchema];
Then import the other Schemas and add them to the array.
// src/graphql/schema/index.ts
import { gql } from "apollo-server-micro";
import noteSchema from "./note";
import customSchema from "./custom";
const linkSchema = gql`
type Query {
_: Boolean
}
type Mutation {
_: Boolean
}
type Subscription {
_: Boolean
}
`;
export default [linkSchema, noteSchema, customSchema];
Setup the resolvers
Since we added a custom type for dates and times in our schema, the first resolver we'll add is for those types. Create a folder called resolvers
in the graphql
folder and in it create custom.ts
.
// src/graphql/resolvers/custom.ts
import { GraphQLDate, GraphQLTime, GraphQLDateTime } from "graphql-iso-date";
export default {
Date: GraphQLDate,
Time: GraphQLTime,
DateTime: GraphQLDateTime,
};
Next we'll start working on the resolver for Notes.
Start by creating note.ts
in graphql
, importing the required dependencies and exporting an empty object which is going to be our resolver.
// src/graphql/resolvers/note.ts
import { ApolloError } from "apollo-server-micro";
import { PrismaClient, Note } from "@prisma/client";
const prisma = new PrismaClient();
export default {};
Notice the
Note
type from@prisma/client
this comes from generating the Prisma Client from earlier.
Then define an object for the Queries.
// src/graphql/resolvers/note.ts
// ...
export default {
Query: {},
};
When defining the function for each of the queries (or mutations or fields) there are three important parameters.
-
parent
- If you a resolving a field of an object this parameter will contain it. We won't be needing it in this post. -
args
- The arguments passed to the query or mutation. -
context
- The context object created when setting up the API. We won't be needing it in this post.
First let's write the resolver for getAllNotes
. Start by declaring the function.
// src/graphql/resolvers/note.ts
// ...
export default {
Query: {
getAllNotes: async (): Promise<Note[]> => {},
},
};
Then add the code to get all the notes.
// src/graphql/resolvers/note.ts
// ...
export default {
Query: {
getAllNotes: async (): Promise<Note[]> => {
try {
const notes = await prisma.note.findMany();
return notes;
} catch (error) {
console.error("> getAllNotes error: ", error);
throw new ApolloError("Error retrieving all notes");
}
},
},
};
After that let's add the resolver for getNote
.
// src/graphql/resolvers/note.ts {20-37}
// ...
export default {
Query: {
// ...
getNote: async (
parent: any,
{ id }: { id: Note["id"] }
): Promise<Note | null> => {
try {
const note = await prisma.note.findUnique({
where: {
id,
},
});
return note;
} catch (error) {
console.error("> getNote error: ", error);
throw new ApolloError("Error retrieving note");
}
},
},
};
We can retrieve the id
argument from the second parameter.
Let's finish of the Note resolvers by adding the resolvers for the mutations.
// src/graphql/resolvers/note.ts
// ...
export default {
// ...
Mutation: {
saveNote: async (
parent: any,
{ title, content }: { title: Note["title"]; content: Note["content"] }
): Promise<Note> => {
try {
const note = await prisma.note.create({
data: {
title,
content,
},
});
return note;
} catch (error) {
console.error("> saveNote error: ", error);
throw new ApolloError("Error creating note");
}
},
deleteNote: async (
parent: any,
{ id }: { id: Note["id"] }
): Promise<Note> => {
try {
const note = await prisma.note.delete({
where: {
id,
},
});
return note;
} catch (error) {
console.error("> getNote error: ", error);
throw new ApolloError("Error retrieving all notes");
}
},
},
};
In the end the file should look like this.
import { ApolloError } from "apollo-server-micro";
import { PrismaClient, Note } from "@prisma/client";
const prisma = new PrismaClient();
export default {
Query: {
getAllNotes: async (): Promise<Note[]> => {
try {
const notes = await prisma.note.findMany();
return notes;
} catch (error) {
console.error("> getAllNotes error: ", error);
throw new ApolloError("Error retrieving all notes");
}
},
getNote: async (
parent: any,
{ id }: { id: Note["id"] }
): Promise<Note | null> => {
try {
const note = await prisma.note.findUnique({
where: {
id,
},
});
return note;
} catch (error) {
console.error("> getNote error: ", error);
throw new ApolloError("Error retrieving note");
}
},
},
Mutation: {
saveNote: async (
parent: any,
{ title, content }: { title: Note["title"]; content: Note["content"] }
): Promise<Note> => {
try {
const note = await prisma.note.create({
data: {
title,
content,
},
});
return note;
} catch (error) {
console.error("> saveNote error: ", error);
throw new ApolloError("Error creating note");
}
},
deleteNote: async (
parent: any,
{ id }: { id: Note["id"] }
): Promise<Note> => {
try {
const note = await prisma.note.delete({
where: {
id,
},
});
return note;
} catch (error) {
console.error("> getNote error: ", error);
throw new ApolloError("Error retrieving all notes");
}
},
},
};
Now we have to connect all the resolvers together using an index.ts
file created in the resolvers
folder.
// src/graphql/resolvers/index.ts
import noteResolver from "./note";
import customResolver from "./custom";
export default [noteResolver, customResolver];
Running the API locally
To run the project locally first you need to link it to a Vercel project.
vercel link
Then run vercel dev
to start the API locally.
vercel dev
If you visit http://localhost:3000/api/graphql
you can see the GraphQL Playground.
Deploying to Vercel
First we need to upload the database path as an Environment Variable.
vercel env add
Name the variable DB_PATH
and make sure you make to available for all three environments (Production, Preview and Development).
Then all that's left to do is to deploy to Vercel.
vercel
The GraphQL Playground should be visible in the /api/graphql
route of the URL returned.
Wrapping Up
I made a sample deployment which you can check out here. The source code is available on GitHub.
If you want a more detailed explanation into GraphQL and making a server for it, you can check out the excellent guide that I learnt from here.
Bonus: Using it in Next.js
You can create a GraphQL API endpoint in your Next.js project by putting the graphql.ts
inside the api
folder in the pages
folder.
The only extra step you need to do is to add this bit of code to the end of the graphql.ts
file.
// pages/api/graphql.ts
// ...
export default apolloServer.createHandler({ path: "/api/graphql" });
export const config = {
api: {
bodyParser: false,
},
};