window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster already exists') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() Deploy GraphQL/MongoDB API using Netlify Functions - The Codest
The Codest
  • About us
  • Services
    • Software Development
      • Frontend Development
      • Backend Development
    • Staff Augmentation
      • Frontend Developers
      • Backend Developers
      • Data Engineers
      • Cloud Engineers
      • QA Engineers
      • Other
    • It Advisory
      • Audit & Consulting
  • Industries
    • Fintech & Banking
    • E-commerce
    • Adtech
    • Healthtech
    • Manufacturing
    • Logistics
    • Automotive
    • IOT
  • Value for
    • CEO
    • CTO
    • Delivery Manager
  • Our team
  • Case Studies
  • Know How
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About us
  • Services
    • Software Development
      • Frontend Development
      • Backend Development
    • Staff Augmentation
      • Frontend Developers
      • Backend Developers
      • Data Engineers
      • Cloud Engineers
      • QA Engineers
      • Other
    • It Advisory
      • Audit & Consulting
  • Value for
    • CEO
    • CTO
    • Delivery Manager
  • Our team
  • Case Studies
  • Know How
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
Back arrow GO BACK
2021-05-13
Software Development

Deploy GraphQL/MongoDB API using Netlify Functions

The Codest

Pawel Rybczynski

Software Engineer

Goals Initial setup Install dependencies Let’s start First, add the tsconfig.json to main directory: Now, let’s create src/server.ts for servers implementations. Then add two functions: one for local server and second for lambda. OK, we don’t have any resolvers or type definitions so we need to create some. Let’s assume that, at first, we want […]

Goals

  1. Configure both local and lambda servers.
  2. Connect both to MongoDB.
  3. Implement basic authentication.
  4. Deploy serverless Apollo GraphQL API with Netlify.
  5. Use Typescript.

Initial setup

npm init -y

Install dependencies

npm install --save typescript graphql aws-lambda @types/aws-lambda

Let’s start

First, add the tsconfig.json to main directory:

 {
 "compilerOptions": {
 "target": "es5",
 "module": "commonjs",
 "allowJs": true,
 "strict": true,
 "esModuleInterop": true,
 "skipLibCheck": true,
 "forceConsistentCasingInFileNames": true
 },
 "include": ["src/*.ts", "src/**/*.ts", "src/**/*.js"],
 "exclude": ["node_modules"]
 }

Now, let’s create src/server.ts for servers implementations. Then add two functions: one for local server and second for lambda.

// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } from "apollo-server";


const createLambdaServer = () =>
  new ApolloServerLambda({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    },
  });

const createLocalServer = () =>
  new ApolloServer({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    },
  });

export { createLambdaServer, createLocalServer };

OK, we don’t have any resolvers or type definitions so we need to create some. Let’s assume that, at first, we want to create users and receive info about them.

// src/schemas.ts
const { gql } = require("apollo-server-lambda");

const userSchema = gql`
  type User {
    id: ID!
    email: String!
    name: String!
  }

  type Query {
    user(id: ID!): User!
  }

  type Mutation {
    createUser(name: String!, email: String!, password: String!): User!
  }
`;

If you’re not familiar with this, Apollo prepared a very nice tutorial

Now let’s create a user resolver with one query and one mutation.

// src/resolvers.ts
const userResolver = {
  Query: {
    user: async (parent, args, context, info) => {
      {...}
    },
  },
  Mutation: {
    createUser: async (parent, args, context, info) => {
      {...}
    },
  },
};

But we don’t have any data… Let’s fix it 😉

Don’t forget to import type definition and resolver onto the server.

// src/server.ts
import { ApolloServer as ApolloServerLambda } from "apollo-server-lambda";
import { ApolloServer } from "apollo-server";

import { typeDefs } from "./schemas";
import { resolvers } from "./resolvers";

{...}

Connect with MongoDB via mongoose

Now it’s a good time to create a connection with our database. In this particular case, it will be MongoDB. It’s free and easy to maintain. But before that, let’s install two more dependencies:

npm install --save mongoose dotenv

The first step is to create a User Model.

// src/model.ts
import mongoose, { Document, Error, Schema } from "mongoose";

export type User = Document & {
  _id: string,
  email: string,
  name: string,
  password: string,
};

delete mongoose.connection.models["User"];

const UserSchema: Schema = new Schema({
  email: {
    type: String,
    required: true,
    unique: true,
  },
  name: {
    type: String,
    required: true,
    minLength: 3,
    maxLength: 32,
  },
  password: {
    type: String,
    required: true,
  },
});

export const userModel = mongoose.model < User > ("User", UserSchema);

Make passwords more safe

Security first! Let’s now secure our passwords by hashing them.

npm install --save bcrypt @types/bcrypt

Now, implement the password security inside the pre-middleware callback. Pre-middleware functions are executed one after another, when each middleware calls next. To make the passwords safe, we are using a technique which generates a salt and hash on separate function calls.

// src/model.ts
import bcrypt from "bcrypt";
{...}
const SALT_WORK_FACTOR: number = 10;

UserSchema.pre("save", function (next) {
  const user = this as User;
  if (!this.isModified("password")) return next();

  bcrypt.genSalt(SALT_WORK_FACTOR, function (err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, function (err, hash) {
      if (err) return next(err);
      user.password = hash;
      next();
    });
  });
});
{...}

Then add comparePasswords method to UserSchema:

// src/model.ts
{...}
UserSchema.methods.comparePasswords = function (
  candidatePassword: string,
  cb: (err: Error | null, same: boolean | null) => void
) {
  const user = this as User;
  bcrypt.compare(candidatePassword, user.password, (err, isMatch) => {
    if (err) {
      return cb(err, null);
    }
    cb(null, isMatch);
  });
};
{...}

And of course, modify User type.

type comparePasswordFunction = (
  candidatePassword: string,
  cb: (err: Error, isMatch: boolean) => void
) => void;

export type User = Document & {
  _id: string,
  email: string,
  name: string,
  password: string,
  comparePasswords: comparePasswordFunction,
};

Now we can arrange a connection between servers and databases. MONGODB_URI is an environment variable which contains a connection string needed to create the connection. You can get it from your cluster panel after you log into your MongoDB atlas account. Put it inside .env

// .env
MONGODB_URI = ...;

Always remember to add that file to .gitignore. Great! Now let’s add a function which allow to connect with db.

// src/server.ts
import mongoose, { Connection } from "mongoose";
{...}
let cachedDb: Connection;

const connectToDatabase = async () => {
  if (cachedDb) return;

  await mongoose.connect(process.env.MONGODB_URI || "", {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
    useCreateIndex: true,
  });
  cachedDb = mongoose.connection;
};
{...}

The context is an object which is shared across all resolvers. To provide it, we just need to add a context initialization function to the ApolloServer constructor. Let’s do it.

// src/server.ts
import { userModel } from "./models/user.model";
{...}
const createLambdaServer = async () =>
  new ApolloServerLambda({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    context: async () => {
      await connectToDatabase();

      return {
        models: {
          userModel,
        },
      };
    },
  });

const createLocalServer = () =>
  new ApolloServer({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    context: async () => {
      await connectToDatabase();

      return {
        models: {
          userModel,
        },
      };
    }
  });

As you can see, we are also passing userModel through context. We can now update resolver:

// resolvers.ts
const userResolver = {
  Query: {
    user: async (_, { email, name, password }, { models: { userModel } }) => {
      const user = await userModel.findById({ _id: id }).exec();
      return user;
    },
  },
  Mutation: {
    createUser: async (_, { id }, { models: { userModel } }) => {
      const user = await userModel.create({ email, name, password });
      return user;
    },
  },
};

Looks nice! Now create a basic server instance:

// src/index.ts
import { createLocalServer } from "./server";
require("dotenv").config();

const port = process.env.PORT || 4000;

const server = createLocalServer();

server.listen(port).then(({ url }) => {
  console.log(`Server ir running at ${url}`);
});

Starting the local server with Nodemon

Last thing before run, add nodemon.json

{
  "watch": ["src"],
  "ext": ".ts,.js",
  "ignore": [],
  "exec": "ts-node --transpile-only ./src/index.ts"
}

Add script to package.json

"scripts": {
    "start": "nodemon"
  },

And run!

npm start

You should get such info inside terminal:

Server ir running at http://localhost:4000/

If yes, open the http://localhost:4000/ inside your browser.

The GraphQL playground should appear. Let’s create a new user!

createUser.png

Take a look at how it looks in the database.

databaseJohnDoe.png

Cool! Everything works fine!

Let’s try and get some user info.

userWithoutAuth.png

And add the password field…

userPassword.png

Nice! We receive error Cannot query field "password" on type "User".". As you can check back, we didn’t add this field inside the user type definition. It is there on purpose. We should not make it possible to query any password or other sensitive data.

Another thing… We can get user data without any authentication… it is not a good solution. We need to fix it.

But before…

Configure Codegen

Let’s use the GraphQL code generator to get a compatible base type, based on our schema.

npm install --save @graphql-codegen/cli @graphql-codegen/introspection
@graphql-codegen/typescript @graphql-codegen/typescript-resolvers

Create codegen.yml

overwrite: true
schema: "http://localhost:4000"
generates:
  ./src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-resolvers"
  ./graphql.schema.json:
    plugins:
      - "introspection"

Add codegen script to package.json

"scripts": {
    "start": "nodemon",
    "codegen": "graphql-codegen --config ./codegen.yml",
  },

Then, when the local server is running, run script:

npm run codegen

In case of success you will get message:

  √ Parse configuration
  √ Generate outputs

If you receive that info, two files should appear:

  • graphql.schema.json in the main directory
  • graphql.ts in newly created path src/generated

We are more interested in the second one. If you open it, you will notice a nice structure of types.

Now we can improve our resolvers:

// src/resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql";

const userResolver: Resolvers = {
  Query: {
    user: async (_, { id }, { models: { userModel }, auth }): Promise<User> => {
      const user = await userModel.findById({ _id: id }).exec();
      return user;
    },
  },
  Mutation: {
    createUser: async (
      _,
      { email, name, password },
      { models: { userModel } }
    ): Promise<User> => {
      const user = await userModel.create({
        email,
        name,
        password,
      });
      return user;
    },
  },
};

Authentication

Next, let’s set up a simple token-based authentication.

npm install --save jsonwebtoken @types/jsonwebtoken

Create checkAuth function to verify if the token is valid. We will add result to the context so that we can access it inside the resolvers.

// src/server.ts
import { IncomingHttpHeaders } from "http";
import {
  APIGatewayProxyEvent,
  APIGatewayProxyEventHeaders,
  Context,
} from "aws-lambda";
import jwt from "jsonwebtoken";
{...}
const checkAuth = async ({ token } : APIGatewayProxyEventHeaders | IncomingHttpHeaders ) => {
  if (typeof token === "string") {
    try {
      return await jwt.verify(token, "riddlemethis");
    } catch (e) {
      throw new AuthenticationError(`Your session expired. Sign in again.`);
    }
  }
};
{...}
const createLambdaServer = async (
  { headers }: APIGatewayProxyEvent,
  context: Context
) => {
  return new ApolloServerLambda({
    typeDefs,
    resolvers,
    context: async () => {
      await connectToDatabase();

      const auth = await checkAuth(headers);

      return {
        auth,
        models: {
          userModel,
        },
      };
    },
  });
};

function createLocalServer() {
  return new ApolloServer({
    typeDefs,
    resolvers,
    introspection: true,
    playground: true,
    context: async ({ req: { headers } = {} }) => {
      const auth = await checkAuth(headers);
      await connectToDatabase();

      return {
         auth,
         models: {
          userModel,
        },
      };
    },
  });
}

Also, we need a way to create such a token. The best way is to implement a login query inside our resolver.

// resolvers.ts
import { Resolvers, Token, User } from "./generated/graphql";

const userResolver: Resolvers = {
  Query: {
    user: async (_, { id }, { models: { userModel }, auth }): Promise<User> => {
      if (!auth) throw new AuthenticationError("You are not authenticated");

      const user = await userModel.findById({ _id: id }).exec();
      return user;
    },
    login: async (
      _,
      { email, password },
      { models: { userModel } }
    ): Promise<Token> => {
      const user = await userModel.findOne({ email }).exec();

      if (!user) throw new AuthenticationError("Invalid credentials");

      const matchPasswords = bcrypt.compareSync(password, user.password);

      if (!matchPasswords) throw new AuthenticationError("Invalid credentials");

      const token = jwt.sign({ id: user.id }, "riddlemethis", {
        expiresIn: 60,
      });

      return { token };
    },
  },
  Mutation: {
    createUser: async (
      _,
      { email, name, password },
      { models: { userModel } }
    ): Promise<User> => {
      const user = await userModel.create({
        email,
        name,
        password,
      });
      return user;
    },
  },
};

You need also to update user type definitions by Token type and login query.

type Token {
    token: String!
  }

type Query {
    user(id: ID!): User!
    login(email: String!, password: String!): Token!
  }

Let’s now try to get user without token

noAuth.png

OK, works fine! Try to log in

token.png

Let’s try again to get user, but this time with token added to headers

withToken.png

Cool! And what if we set wrong credentials?

wrongEmail.png

Nice!

Prepare to Deploy

Last step: deploy serverless api with Netlify!

Create folder lambda in main dir and put two files inside:

First contains AWS handler. It create a ApolloServerLambda server instance and
then expose a handler using createHandler of that instance.

// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createLambdaServer } from "???";

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  const server = await createLambdaServer(event, context);

  return new Promise((res, rej) => {
    const cb = (err: Error, args: any) => (err ? rej(err) : res(args));
    server.createHandler()(event, context, cb);
  });
};

You can read more about it.

Second one, is the tsconfig. Important part is the outDir field.

// lambda/tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es6",
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "outDir": "./dist"
  },
  "include": ["./*.ts", "./**/*.ts", "./**/*.js"]
}

But there’s a problem. We can’t use /src dir because Netlify can’t reach outside of the lambda folder. So we must bundle it.

npm install --save ncp

It`s a package that allows as to copy directory.

Add bundle script to package.json

"scripts": {
    "bundle": "ncp ./src ./lambda/bundle",
    "codegen": "graphql-codegen --config ./codegen.yml",
    "start": "nodemon",
  },

Now if you run npm run bundle, you san see, that in newly created lambda/bundle dir we have all files from src/.

Update the import path inside lambda/graphql.ts

// lambda/graphql.ts
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { createLambdaServer } from "./bundle/server";

{...}

You can now add lambda/bundle dir to .gitignore

Deploy with Netlify

We must to tell Netlify what the build command is and where our functions live. To to it let’s create netlify.toml file:

// netlify.toml
[build]
  command = "npm run build:lambda"
  functions = "lambda/dist"

As you can see, it’s same directory as defined as a outDir field in lambda/tsconfig.json

This is how your app structure should look like (well… part of it ;))

app
└───lambda
│ └────bundle
│ │ graphql.ts
│   tsconfig.json.ts
└───src
│ └───generated
│ │    │ graphql.ts
│ │ index.ts
│ │ model.ts
│ │ resolvers.ts
│ │ schemas.ts
│   server.ts
│
│ codegen.yml
│ graphql.schema.json
│ netlify.toml
│ nodemon.json
│ tsconfig.json

Add bundle:lambda script to package.json

"scripts": {
    "build:lambda": "npm run bundle && tsc -p lambda/tsconfig.json",
    "bundle": "ncp ./src ./lambda/bundle",
    "codegen": "graphql-codegen --config ./codegen.yml",
    "start": "nodemon",
  },

Deploy

Let’s try deploying our app via Netlify.

  1. Log to your Netlify account,
  2. Connect with your Github,
  3. Click ‘New site from Git’ button,
  4. Select a proper repo,
  5. Set the build command npm run build:lambda,
  6. Add environment variable (MONGODB_URI),
  7. Deploy…

And TAAADAAAA…

pageNotFound.png

That is because the endpoint is by default not the http://page/ but http://page/.netlify/functions/graphql.

withoutRedirect.png

How to fix it? It’s very simple. Just create _redirects with:

/ /.netlify/functions/graphql 200!

Deploy again and check.

redirect.png

Hope you liked it! Feel free to improve and change.

Read more:

How not to kill a project with bad coding practices?

Web app security. Target=”_blank” vulnerability

Web app security – XSS vulnerability

Related articles

Software Development

Build Future-Proof Web Apps: Insights from The Codest’s Expert Team

Discover how The Codest excels in creating scalable, interactive web applications with cutting-edge technologies, delivering seamless user experiences across all platforms. Learn how our expertise drives digital transformation and business...

THECODEST
Software Development

Top 10 Latvia-Based Software Development Companies

Learn about Latvia's top software development companies and their innovative solutions in our latest article. Discover how these tech leaders can help elevate your business.

thecodest
Enterprise & Scaleups Solutions

Java Software Development Essentials: A Guide to Outsourcing Successfully

Explore this essential guide on successfully outsourcing Java software development to enhance efficiency, access expertise, and drive project success with The Codest.

thecodest
Software Development

The Ultimate Guide to Outsourcing in Poland

The surge in outsourcing in Poland is driven by economic, educational, and technological advancements, fostering IT growth and a business-friendly climate.

TheCodest
Enterprise & Scaleups Solutions

The Complete Guide to IT Audit Tools and Techniques

IT audits ensure secure, efficient, and compliant systems. Learn more about their importance by reading the full article.

The Codest
Jakub Jakubowicz CTO & Co-Founder

Subscribe to our knowledge base and stay up to date on the expertise from the IT sector.

    About us

    The Codest – International software development company with tech hubs in Poland.

    United Kingdom - Headquarters

    • Office 303B, 182-184 High Street North E6 2JA
      London, England

    Poland - Local Tech Hubs

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warsaw, Poland

      The Codest

    • Home
    • About us
    • Services
    • Case Studies
    • Know How
    • Careers
    • Dictionary

      Services

    • It Advisory
    • Software Development
    • Backend Development
    • Frontend Development
    • Staff Augmentation
    • Backend Developers
    • Cloud Engineers
    • Data Engineers
    • Other
    • QA Engineers

      Resources

    • Facts and Myths about Cooperating with External Software Development Partner
    • From the USA to Europe: Why do American startups decide to relocate to Europe
    • Tech Offshore Development Hubs Comparison: Tech Offshore Europe (Poland), ASEAN (Philippines), Eurasia (Turkey)
    • What are the top CTOs and CIOs Challenges?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Website terms of use

    Copyright © 2025 by The Codest. All rights reserved.

    en_USEnglish
    de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek en_USEnglish