DEV Community

Cover image for Create a serverless API with TypeScript, GraphQL and MongoDB
Akhila Ariyachandra
Akhila Ariyachandra

Posted on • Updated on • Originally published at akhilaariyachandra.com

Create a serverless API with TypeScript, GraphQL and MongoDB

PS - This was originally posted in my blog. Check it out if you want learn more about React and JavaScript!

In this post we'll create a GraphQL API to create, view and delete notes that we can deploy to Zeit using Apollo Server, TypeScript and MongoDB.

Initial Setup

First the Now CLI will need be downloaded, installed and logged into.

yarn global add now
now login
Enter fullscreen mode Exit fullscreen mode

The nice thing about using the Now CLI 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
yarn init --yes
yarn add typescript -D
Enter fullscreen mode Exit fullscreen mode

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.

yarn add mongoose
yarn add @types/mongoose -D
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

Next create an interface for the model which extends from mongoose.Document. We'll need to export this interface for use later as well.

export interface INote extends mongoose.Document {
  title: string;
  content: string;
  date: Date;
}
Enter fullscreen mode Exit fullscreen mode

After that we'll create the schema definition.

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 }
};
Enter fullscreen mode Exit fullscreen mode

Then define the name of the collection and the schema using schema definition.

const collectionName: string = "note";
const noteSchema: mongoose.Schema = new mongoose.Schema(schema);
Enter fullscreen mode Exit fullscreen mode

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 functions, make it the default export.

const Note = (conn: mongoose.Connection): mongoose.Model<INote> =>
  conn.model(collectionName, noteSchema);

export default Note;
Enter fullscreen mode Exit fullscreen mode

Finally the Note model file should look like this.

// src/database/models/note.ts
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;
Enter fullscreen mode Exit fullscreen mode

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.

Never commit the .env file. Make sure to add it to .gitignore.

DB_PATH=mongodb://user:password@ds654321.mlab.com:12345/example-db
Enter fullscreen mode Exit fullscreen mode

Next create the the index.js file in src/database and import mongoose.

import mongoose from "mongoose";
Enter fullscreen mode Exit fullscreen mode

Then get the MongoDB URI that we included in the .env file. It can be accessed as a key in process.env.

const uri: string = process.env.DB_PATH;
Enter fullscreen mode Exit fullscreen mode

After that declare a variable that will be used to cache the database connection between function invocations to prevent overloading the database.

let conn: mongoose.Connection = null;
Enter fullscreen mode Exit fullscreen mode

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.

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;
};
Enter fullscreen mode Exit fullscreen mode

Finally the file should look like this.

// src/database/index.ts
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;
};
Enter fullscreen mode Exit fullscreen mode

Setup the GraphQL API

First let's install the dependencies.

yarn add apollo-server-micro dayjs graphql graphql-iso-date
Enter fullscreen mode Exit fullscreen mode

Next create the folder api and in it create the file graphql.ts. Now 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";
Enter fullscreen mode Exit fullscreen mode

After that import the GraphQL Schemas and Resolvers. We'll create these later.

import typeDefs from "../src/graphql/schema";
import resolvers from "../src/graphql/resolvers";
Enter fullscreen mode Exit fullscreen mode

Then initialize the apollo server and export it.

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers
});

export default apolloServer.createHandler({ path: "/api/graphql" });
Enter fullscreen mode Exit fullscreen mode

When creating a new ApolloServer, we can define the context option. The context option 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.

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  context: async () => {
    const dbConn = await getConnection();

    return { dbConn };
  }
});
Enter fullscreen mode Exit fullscreen mode

To finish the function code we'll set the introspection and playground options.

const apolloServer = new ApolloServer({
  typeDefs,
  resolvers,
  context: async () => {
    const dbConn = await getConnection();

    return { dbConn };
  },
  playground: true,
  introspection: true
});
Enter fullscreen mode Exit fullscreen mode

Finally the file should look like this.

// api/graphql.ts
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" });
Enter fullscreen mode Exit fullscreen mode

Setup the GraphQL Schema

In the src folder create the graphql folder and in that 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
`;
Enter fullscreen mode Exit fullscreen mode

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``;
Enter fullscreen mode Exit fullscreen mode

Then add the Note type.

export default gql`
  type Note {
    _id: ID!
    title: String!
    content: String!
    date: DateTime!
  }
`;
Enter fullscreen mode Exit fullscreen mode

After that add two Queries, one to get all Notes and one to get a specific Note.

export default gql`
  extend type Query {
    getAllNotes: [Note!]
    getNote(_id: ID!): Note
  }

  type Note {
    _id: ID!
    title: String!
    content: String!
    date: DateTime!
  }
`;
Enter fullscreen mode Exit fullscreen mode

Next add two Mutations to create new Notes and delete existing ones.

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!
  }
`;
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

Next we'll start working on the resolver for Notes.

Start by 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 {};
Enter fullscreen mode Exit fullscreen mode

Then define an object for the Queries.

export default {
  Query: {}
};
Enter fullscreen mode Exit fullscreen mode

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.

export default {
  Query: {getAllNotes: async (
      parent,
      args,
      context
    ): Promise<INote[]> => {}
};
Enter fullscreen mode Exit fullscreen mode

Let's destructure the context object.

export default {
  Query: {getAllNotes: async (
      parent,
      args,
      { dbConn }: { dbConn: mongoose.Connection }
    ): Promise<INote[]> => {}
};
Enter fullscreen mode Exit fullscreen mode

In the function we should create the Note model using the database connection from the context and retrieve all the Notes using it.

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;
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

After that let's add the resolver for getNote.

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");
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

We can retrieve the id argument from the second parameter.

Let's finish of the Note resolvers by adding the resolvers for the mutations.

export default {
  // ..........
  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");
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally the file should look like this.

// src/graphql/resolvers/note.ts
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");
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

Running the API locally

Just run now locally.

now dev
Enter fullscreen mode Exit fullscreen mode

If you visit http://localhost:3000/api/graphql you can see the GraphQL Playground.

Deploying to Now

First we need to upload the database path as a secret. We upload it as serverless-graphql-api-example-dp-path.

now secrets add serverless-graphql-api-example-dp-path "DB_PATH=mongodb://user:password@ds654321.mlab.com:12345/example-db"
Enter fullscreen mode Exit fullscreen mode

Then create the now configuration file, now.json in the project root.

{
  "version": 2,
  "name": "serverless-graphql-api-example",
  "env": {
    "DB_PATH": "@serverless-graphql-api-example-dp-path"
  }
}
Enter fullscreen mode Exit fullscreen mode

In the configuration file, we specifying to expose the secret serverless-graphql-api-example-dp-path as environment variable DB_PATH.

All that's let to do is deploying to now.

now
Enter fullscreen mode Exit fullscreen mode

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 learned from here.

Discussion (5)

Collapse
jossdz profile image
Jose Carlos Correa • Edited on

Does this exact example may fit in a next route api ?
Do you have an example about how to test this?

Collapse
akhilaariyachandra profile image
Akhila Ariyachandra Author

Yes you can use this in a Next.js API route. Just make sure to this to the end of the graphql.ts file.

export const config = {
  api: {
    bodyParser: false,
  },
}
Collapse
jossdz profile image
Jose Carlos Correa

Thank you !

I'm having an issue with the deploy, hope to fix it soon.

Collapse
yongyi520 profile image
yongyi520

Hi Thank you so much for this post. This is exactly what I have been looking for! Much much appreciated!

Collapse
akhilaariyachandra profile image
Akhila Ariyachandra Author

Thanks!!!! Glad you found it useful!!! 😊