Most documentation for GraphQL tends to address the basics of Querying and Mutation with some additional blog posts about deploying an API. The issue I recently ran into was trying to implement GraphQL-yoga on AWS Lambda backed by a database other than DynamoDB. This project will focus on implementing a mongo database hosted on MLab and connected with Mongoose.

Starting Out

If you want to take a look at the finished code for the project check out https://github.com/resputin/yoga-serverless-mongo. To run the project locally:

Clone https://github.com/resputin/yoga-serverless-mongo

yarn install

Configure envs for MONGO_DB_URL and SESSION_SECRET

and yarn start

Go to localhost:3000 and the project will boot into graphql-playground to play around with. The schema tab will include the relevant information to build queries and mutations.

The Schema

This will be a simple implementation of a To Do application. Our Schema will look like this

type User { _id: ID! email: String! todos: [Todo!]! } type Todo { _id: ID! content: String! } type AuthPayload { token: String! user: User }

This will set up our data model. We have a User that we can query id, email, and todos on. We have a Todo that we can query id and content. Finally we have an AuthPayload that we can query a JWT and a User.

type Query { todos: [Todo!]! todo(_id: ID!): Todo! } type Mutation { signup(email: String!, password: String!): AuthPayload login(email: String!, password: String!): AuthPayload createTodo(content: String!): Todo! }

For our root level Queries we can query allTodos which will return a list of Todo’s or todo which will return a specific Todo from its id.

This schema will get imported into our handler file onto yoga’s GraphQLServerLambda class. This class will convert our schema and resolvers into a graphQL server that will interface with Lambda.

const lambda = new GraphQLServerLambda({ typeDefs: './schema.graphql'

// more here later });

Resolvers

Our resolvers will also be relatively straightforward. Yoga will handle our simple query resolvers automatically, so all we need to worry about are our Todo’s and User Auth.

Our Todo queries are straightforward

const todos = async (parent, args, context) => {

const userId = authenticate(context); try {

const user = await User.findOne({ _id: userId }).lean()

return user.todos;

} catch (err) {

throw new Error(err);

}

}; const todo = async (parent, { _id }, context) => {

const userId = authenticate(context);

try {

const user = await User.findOne({ _id: userId }).lean();

const todo = await user.todos.find(

todo => todo._id.toString() === _id

);

return todo;

} catch (err) {

throw new Error(err);

}

};

These queries are authenticated which we can see here in utils .

const authenticate = context => {

const Authorization = context.event.headers.Authorization; if (Authorization) {

const token = Authorization.replace('Bearer ', '');

const { userId } = jwt.verify(token, config.SESSION_SECRET);

return userId;

} throw new Error('Not authorized');

};

An important note here is that we will be attaching our request to our context object to get the Bearer token from our header, so let’s do that now in handler

const lambda = new GraphQLServerLambda({

typeDefs: './schema.graphql',

context: req => ({ ...req })

});

Our mutations are straightforward as well

const createTodo = async (_, { content }, context) => {

const userId = authenticate(context);

try {

const user = await User.findOne({ _id: userId });

const newTodo = { _id: new ObjectId(), content };

user.todos.push(newTodo);

await user.save();

return newTodo;

} catch (err) {

throw new Error(err);

}

}; const signup = async (_, { email, password }) => {

try {

let user = await User.findOne({ email }).lean(); if (user) {

throw new Error('Email is already taken');

} const _password = await bcrypt.hash(password, 10);

user = await new User({ email, password: _password }).save(); const token = jwt.sign({ userId: user._id }, config.SESSION_SECRET); return { token, user };

} catch (err) {

throw new Error(err);

}

}; const login = async (_, { email, password }) => {

try {

const user = await User.findOne({ email }).lean();

if (!user) {

throw new Error('No such user found');

} const valid = await bcrypt.compare(password, user.password);

if (!valid) {

throw new Error('Invalid password');

} user.password = undefined; return {

token: jwt.sign({ userId: user._id }, config.SESSION_SECRET),

user

};

} catch (err) {

throw new Error(err);

}

};

With our resolvers finished we can add them to our handler and have a completed GraphQL Server (albeit non-functional)

const resolvers = {

Query,

Mutation

}; const lambda = new GraphQLServerLambda({

typeDefs: './schema.graphql',

resolvers,

context: req => ({ ...req })

});

Mongoose

For mongoose we just need to define a User model that matches with our GraphQL schema, just adding passwords

const userSchema = new mongoose.Schema({

email: { type: String, unique: true },

password: { type: String, select: false },

todos: [

{

content: { type: String, required: true }

}

]

}); const User = mongoose.model('User', userSchema);

Serverless Fun

With the set up out of the way, let’s get to work on spinning up our Mongo connection and deploying to Lambda. In handler we need to connect to our database which will then stay ‘warmed up’ for all subsequent requests we make to our endpoint.

async function start() {

const mongoClient = await mongoose.connect(config.MONGO_DB_URL);

mongoose.connection.on('error', function(err) {

console.log('Mongoose default connection error: ' + err);

});

return true;

}

Now for our Lambda functions. Yoga already provides most of the logic here, we just need to add a couple things so that we ensure connection to our database and since we will have a constant connection to our database we also need to tell Lambda to not wait for our event loop to be empty before returning values to us.

exports.playground = async (event, context, callback) => {

context.callbackWaitsForEmptyEventLoop = false;

await start();

return lambda.playgroundHandler(event, context, callback);

}; exports.server = async (event, context, callback) => {

context.callbackWaitsForEmptyEventLoop = false;

await start();

return lambda.graphqlHandler(event, context, callback);

};

If you already have serverless set up with AWS Lambda you can run yarn deploy and you can explore the graphQL-playground on your AWS endpoint!

To set up the serverless CLI: yarn global add serverless

Then you can set up your serverless credentials with this tutorial: https://serverless.com/framework/docs/providers/aws/guide/credentials/