
Create a serverless API with TypeScript, GraphQL and MongoDB
Posted on
•Last updated on
14 min read
•0 views
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.
npm i -g 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-graphql-api-example
cd serverless-graphql-api-example
npm init --yes
npm install typescript -D
Setup the Database
To start setting up the database first go to MongoDB Atlas, create a database and get the connection URI.
If you want a more detailed explanation check out my previous guide here. Note that you can’t follow it exactly here because you can no longer create new accounts in MLab (use MongoDB Atlas instead) and that guide is for an always running server, not a serverless one.
Then install the mongoose
dependency. Since we’ll be working with TypeScript we’ll need the types for mongoose
as well.
npm install mongoose
npm install @types/mongoose -D
After let’s create the model for the notes. Create a folder called src
. After that create a folder called database
in src
. Then in that create another folder called models
and in that create the file note.ts
.
Start by importing mongoose
.
import mongoose from "mongoose";
Next create an interface for the model which extends from mongoose.Document
. We’ll need to export this interface for use later as well.
import mongoose from "mongoose";
export interface INote extends mongoose.Document {
title: string;
content: string;
date: Date;
}
After that we’ll create the schema definition.
import mongoose from "mongoose";
export interface INote extends mongoose.Document {
title: string;
content: string;
date: Date;
}
const schema: mongoose.SchemaDefinition = {
title: { type: mongoose.SchemaTypes.String, required: true },
content: { type: mongoose.SchemaTypes.String, required: true },
date: { type: mongoose.SchemaTypes.Date, required: true },
};
Then define the name of the collection and the schema using schema definition.
import mongoose from "mongoose";
export interface INote extends mongoose.Document {
title: string;
content: string;
date: Date;
}
const schema: mongoose.SchemaDefinition = {
title: { type: mongoose.SchemaTypes.String, required: true },
content: { type: mongoose.SchemaTypes.String, required: true },
date: { type: mongoose.SchemaTypes.Date, required: true },
};
const collectionName: string = "note";
const noteSchema: mongoose.Schema = new mongoose.Schema(schema);
Since we’re building a serverless API, we can’t depend on a persistent connection to database. When creating a Model we need to create it using the database connection.
To compensate for using serverless we’ll generate the Model at runtime using a function. After declaring the function, make it the default export.
import mongoose from "mongoose";
export interface INote extends mongoose.Document {
title: string;
content: string;
date: Date;
}
const schema: mongoose.SchemaDefinition = {
title: { type: mongoose.SchemaTypes.String, required: true },
content: { type: mongoose.SchemaTypes.String, required: true },
date: { type: mongoose.SchemaTypes.Date, required: true },
};
const collectionName: string = "note";
const noteSchema: mongoose.Schema = new mongoose.Schema(schema);
const Note = (conn: mongoose.Connection): mongoose.Model<INote> =>
conn.model(collectionName, noteSchema);
export default Note;
After creating the Note Model, let’s look into the database connection.
Since we’re using serverless, we can’t have a persistent connection to database for every invocation of the serverless function as this would create issues with scaling.
First create the .env
file in the project root to store the MongoDB URI as DB_PATH
.
Next commit the .env file. Make sure to add it to .gitignore.
DB_PATH=mongodb://user:password@ds654321.mlab.com:12345/example-db
Next create the the index.js
file in src/database
and import mongoose
.
import mongoose from "mongoose";
Then get the MongoDB URI that we included in the .env file. It can be accessed as a key in process.env
.
import mongoose from "mongoose";
const uri: string = process.env.DB_PATH;
After that declare a variable that will be used to cache the database connection between function invocations to prevent overloading the database.
import mongoose from "mongoose";
const uri: string = process.env.DB_PATH;
let conn: mongoose.Connection = null;
Finally we need to create a function that will return a database connection. The function will first check if there is a cached connection. If there is one it will return it or else it will create a new connection and cache and return it. Be sure to export the function.
import mongoose from "mongoose";
const uri: string = process.env.DB_PATH;
let conn: mongoose.Connection = null;
export const getConnection = async (): Promise<mongoose.Connection> => {
if (conn == null) {
conn = await mongoose.createConnection(uri, {
bufferCommands: false, // Disable mongoose buffering
bufferMaxEntries: 0, // and MongoDB driver buffering
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
});
}
return conn;
};
Setup the GraphQL API
First let’s install the dependencies.
npm install apollo-server-micro dayjs graphql graphql-iso-date
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
and the function to create the database connection.
import { ApolloServer } from "apollo-server-micro";
import { getConnection } from "../src/database";
After that import the GraphQL Schemas and Resolvers. We’ll create these later.
import { ApolloServer } from "apollo-server-micro";
import { getConnection } from "../src/database";
import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
Then initialize the Apollo Server and export it.
import { ApolloServer } from "apollo-server-micro";
import { getConnection } from "../src/database";
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" });
When creating a new ApolloServer
, we can define the context
option. The context
is an object which can be accessed by any of the resolvers.
This will be a good place to initialize the database connection and automatically pass it down to the resolvers.
import { ApolloServer } from "apollo-server-micro";
import { getConnection } from "../src/database";
import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: async () => {
const dbConn = await getConnection();
return { dbConn };
},
});
export default apolloServer.createHandler({ path: "/api/graphql" });
To finish configuring we’ll set the introspection
and playground
options.
import { ApolloServer } from "apollo-server-micro";
import { getConnection } from "../src/database";
import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: async () => {
const dbConn = await getConnection();
return { dbConn };
},
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.
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.
import { gql } from "apollo-server-micro";
export default gql``;
Then add the Note type.
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.
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.
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.
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.
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
.
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.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
export default {};
Then define an object for the Queries.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
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. In our case it’ll contain the database connection.
First let’s write the resolver for getAllNotes
. Start by declaring the function.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
export default {
Query: {getAllNotes: async (
parent,
args,
context
): Promise<INote[]> => {}
};
Let’s destructure the context
object.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
export default {
Query: {getAllNotes: async (
parent,
args,
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote[]> => {}
};
In the function we should create the Note model using the database connection from the context and retrieve all the Notes using it.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
export default {
Query: {
getAllNotes: async (
parent,
args,
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote[]> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
let list: INote[];
try {
list = await Note.find().exec();
} catch (error) {
console.error("> getAllNotes error: ", error);
throw new ApolloError("Error retrieving all notes");
}
return list;
},
},
};
After that let’s add the resolver for getNote
.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
export default {
Query: {
getAllNotes: async (
parent,
args,
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote[]> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
let list: INote[];
try {
list = await Note.find().exec();
} catch (error) {
console.error("> getAllNotes error: ", error);
throw new ApolloError("Error retrieving all notes");
}
return list;
},
getNote: async (
parent,
{ _id }: { _id: INote["_id"] },
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
try {
const note = await Note.findById(_id).exec();
return note;
} catch (error) {
console.error("> getNote error: ", error);
throw new ApolloError("Error retrieving all notes");
}
},
},
};
We can retrieve the id
argument from the second parameter.
Let’s finish of the Note resolvers by adding the resolvers for the mutations.
import mongoose from "mongoose";
import dayjs from "dayjs";
import NoteModel, { INote } from "../../database/models/note";
import { ApolloError } from "apollo-server-micro";
export default {
Query: {
getAllNotes: async (
parent,
args,
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote[]> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
let list: INote[];
try {
list = await Note.find().exec();
} catch (error) {
console.error("> getAllNotes error: ", error);
throw new ApolloError("Error retrieving all notes");
}
return list;
},
getNote: async (
parent,
{ _id }: { _id: INote["_id"] },
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
try {
const note = await Note.findById(_id).exec();
return note;
} catch (error) {
console.error("> getNote error: ", error);
throw new ApolloError("Error retrieving all notes");
}
},
},
Mutation: {
saveNote: async (
parent,
{ title, content }: { title: INote["title"]; content: INote["content"] },
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
try {
const note = await Note.create({
title,
content,
date: dayjs().toDate(),
});
return note;
} catch (error) {
console.error("> saveNote error: ", error);
throw new ApolloError("Error creating note");
}
},
deleteNote: async (
parent,
{ _id }: { _id: INote["_id"] },
{ dbConn }: { dbConn: mongoose.Connection }
): Promise<INote> => {
const Note: mongoose.Model<INote> = NoteModel(dbConn);
try {
const note = await Note.findByIdAndDelete(_id).exec();
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.
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 below or 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.
// ...
export default apolloServer.createHandler({ path: "/api/graphql" });
export const config = {
api: {
bodyParser: false,
},
};