Front-End Web & Mobile

The fullstack guide to using AWS AppSync and MongoDB Atlas

AWS AppSync is a fully managed service that makes it simple to build scalable APIs for web and mobile applications. It allows you to create APIs that access data from a variety of sources, including databases, serverless functions, and other AWS services. With AWS AppSync, you can build APIs that support real-time data via WebSockets, so that your applications always have the most up-to-date information. AppSync also integrates well with core application features, such as authentication and authorization, so that you can focus on building the core functionality of your application.

MongoDB is a popular and powerful NoSQL database system. Unlike traditional relational databases, which store data in tables with a fixed number of columns, MongoDB stores data in flexible, JSON-like documents with optional schemas. This means that you can store data in MongoDB without first defining the structure of that data, making it easier to work with and more scalable.

In this post, we’ll discuss how AppSync can integrate with MongoDB to build a function-less API. We’ll also discuss how frontends can leverage the Amplify UI libraries and the new Form Builder ability in Amplify Studio to rapidly connect your frontend with your backend.

To accomplish this, our project will consist of two projects:

  1. A React application that leverages NextJS
  2. A CDK application that provisions our services and setups up our frontend CI/CD pipeline

frontend form with schema

As you can see, our application will be a simple contact form that saves information in a database.

Creating a GraphQL Schema

To understand how this all comes together, it’s important to understand a bit about GraphQL and AppSync. Fortunately, I’ve written about this a bit and have a video that can get you up to speed if you prefer that instead.

Fullstack GraphQL with AWS AppSync and Amplify Youtube thumbnail

The schema needed for our application will be the following:

type Schema {
	query: Query
	mutation: Mutation
}

type Query {
	listApplicants(limit: Int, skip: Int): [Applicant]
}

type Mutation {
	addApplicant(input: ApplicantInput!): Applicant
}

type Applicant {
	id: ID
	name: String
	email: AWSEmail
	phoneNumber: AWSPhone
	employmentStatus: EMPLOYMENT_STATUS_ENUM
	startDate: AWSDate
	focusArea: FOCUS_AREA_ENUM
	createdAt: AWSDateTime
	updatedAt: AWSDateTime
}

input ApplicantInput {
	id: ID
	name: String!
	email: AWSEmail!
	phoneNumber: AWSPhone!
	employmentStatus: EMPLOYMENT_STATUS_ENUM!
	startDate: AWSDate!
	focusArea: FOCUS_AREA_ENUM!
}

enum EMPLOYMENT_STATUS_ENUM {
	EMPLOYED
	UNEMPLOYED
}

enum FOCUS_AREA_ENUM {
	FRONTEND
	BACKEND
}

Based on our schema, our API will consist of two primary actions:

  1. Getting a list of people that applied
  2. Adding an applicant

By securing our API with a simple API key, we can keep it public. For a practical walkthrough on getting this setup in your own project, refer to my previous post. However, if wanting to add more secure mechanisms, refer to this post that shows how to Secure AWS AppSync with Amazon Cognito using the AWS CDK.

Creating our MongoDB Atlas Cluster

To persist our form data, we’ll use a MongoDB Atlas Cluster. MongoDB Atlas is a serverless database service provided by MongoDB. It is a fully managed service that allows you to run a MongoDB database without having to worry about setting up, configuring, and maintaining the underlying infrastructure.

Using the screenshots that follow as guidance, to set up a Shared Atlas cluster we’ll log into the Atlas dashboard and create a new database.

📘 When creating my cluster, note that I changed the name from the default cluster0 to AppSyncMongoTest.

mongo1

With our cluster created, we can create our database and collection. We’ll use the names applicationEmployment employmentForm , respectively.

mongo2

Once done, we’ll want to ensure that our database is enabled to use the Data API. This will expose an endpoint for us to read and write to our database. Under Triggers, select Data API, and toggle the switch so that it’s enabled.

mongo3

Now that our database has the Data API enabled, we can generate an API key with read and write access. Click the Create API Key button on the right-hand side.

mongo4

Give your API key a name of APPSYNC_MONGO_API_KEY as shown below and make a note of the generated API Key. It’s important to copy this value now since it won’t be visible once the screen is cleared. This key provides direct read and write access to our database, as such, we’ll store it in AWS Secret Manager in the following section.

mongo5

🚨 Make sure you copied the API key and not the id as shown in the above screenshot.

Storing a MongoDB API Key in AWS Secrets Manager

AWS Secrets Manager is a managed store for keeping track of sensitive values. Secrets Manager enables you to replace hardcoded credentials in your code, including passwords, with an API call to Secrets Manager to retrieve the secret programmatically. This helps ensure the secret can’t be compromised by someone examining your code, because the secret no longer exists there. Also, you can configure Secrets Manager to automatically rotate the secret for you according to a specified schedule. This enables you to replace long-term secrets with short-term ones, significantly reducing the risk of compromise.

The simplest way to store a secret is to use the AWS CLI. By using the CLI, our MongoDB API key can be stored by issuing the following terminal command:

aws secretsmanager create-secret \
    --name APPSYNC_MONGO_API_KEY \
    --secret-string "my_secret_value" \
    --description "This is my api key for my employmentForm"

As opposed to using the CLI, the API key can be stored via the console via the following steps:

After logging into your AWS account, navigate to the AWS Secrets Manager service and select the option to create a secret.

When prompted to choose a secret type, select Other type of secret, and on the Plaintext tab, paste in your API key from MongoDB Atlas.

secrets-manager

On the next screen, give your secret the name APPSYNC_MONGO_API_KEY and optionally provide a description.

secrets-manager

Connecting to MongoDB Data API with an AppSync HTTP Resolver

pipeline-ad to mongodb

Recently, AppSync launched support for JavaScript resolvers. This enables developers to use a familiar language to develop their APIs, as opposed to using VTL.

JavaScript resolvers, work in connection with pipeline resolvers. I’ve written previously about pipeline resolvers. Feel free to refer to that post or consult the AppSync docs.

In short, a pipeline will allow us to sequence our actions in a before step, a series of functions, and an after step.

Our API contains two operations: listApplicants and addApplicant. Each of these will map to a pipeline.

Creating a reusable pipeline

import { util } from '@aws-appsync/utils'

export function request(ctx) {
	console.log(ctx.info)
	console.log(ctx.args.input)
	switch (ctx.info.fieldName) {
		case 'listApplicants':
			return {}
		case 'addApplicant':
			const applicantData = {
				id: util.autoId(),
				createdAt: util.time.nowISO8601(),
				updatedAt: util.time.nowISO8601(),
				...ctx.args.input,
			}
			ctx.stash.applicantData = applicantData
			return {}
		default:
			return {}
	}
}

export function response(ctx) {
	console.log(ctx.prev.result)
	return ctx.prev.result
}

The ctx object (short for “context”) contains information about the current operation. By logging these values, they are automatically sent to CloudWatch logs due to our API enabling logging.

Another important note is the addition of the util library. This contains handy functions that aid in creating timestamps, unique IDs, and more.

Lastly, as a matter of preference, data is stored in the stash. This caches the values and makes them available to subsequent functions in our pipeline. It’s worth noting that the ctx.arguments are also available throughout the pipeline as well.

Accessing Secrets Manager with an HTTP resolver

When our frontend application calls the addApplicant mutation on our API, we take in their input in the before step above, but we don’t need that until later. Before we can contact MongoDB, we first have to access the API key we stored in secrets Manager.

Our AppSync API is configured with an HTTP datasource. This datasource uses a regional endpoint to make an HTTP request to Secrets Manager. What’s great about this approach is it doesn’t use the public internet and instead uses a sigV4 request across AWS’s internal network. This direct integration reduces cost and latency when compared to making a request with a serverless function.

import { util } from '@aws-appsync/utils'

export function request(ctx) {
	return {
		method: 'POST',
		version: '2018-05-29',
		resourcePath: '/',
		params: {
			headers: {
				'content-type': 'application/x-amz-json-1.1',
				'x-amz-target': 'secretsmanager.GetSecretValue',
			},
			body: {
				SecretId: 'APPSYNC_MONGO_API_KEY',
			},
		},
	}
}

export function response(ctx) {
	//"{Name: 'APPSYNC_MONGO_API_KEY', SecretString: 'abc123'}"
	const result = JSON.parse(ctx.result.body).SecretString
	console.log(result)
	return result
}

In the above template, a POST request is made, and the name of our API key, APPSYNC_MONGO_API_KEY, is passed as the body.

In the response, the body is parsed and the value is passed to the next function in the pipeline.

Insert a document into MongoDB with an HTTP resolver

As opposed to making internal HTTP calls to other AWS services, there are many times when a call to an external HTTP endpoint needs to be made.

In the case of our application, now that our API key has been provided, this function can call the insertOne endpoint.

🗒️ The ctx.prev.result property contains whatever information was passed from the previous function.

import { util } from '@aws-appsync/utils'

export function request(ctx) {
	const secret = ctx.prev.result
	const document = ctx.stash.applicantData
	console.log('the document', document)
	return {
		method: 'POST',
		version: '2018-05-29',
		resourcePath: '/app/data-upuof/endpoint/data/v1/action/insertOne',
		params: {
			headers: {
				'api-key': secret,
				'Content-Type': 'application/json',
				'Access-Control-Request-Headers': '*',
				Accept: 'application/json',
			},
			body: {
                // Recall we renamed from 'Cluster0' to this name
				dataSource: 'AppSyncMongoTest',
				database: 'applicationEmployment',
				collection: 'employmentForm',
				document,
			},
		},
	}
}

export function response(ctx) {
	const res = JSON.parse(ctx.result.body)

	// https://www.mongodb.com/docs/atlas/api/data-api-resources/#response-2
	if (res.insertedId) {
		return ctx.stash.applicantData
	} else {
		util.error('record failed to be inserted')
	}
}

In the response portion of this file, we parse the result.body to get the response from MongoDB. The way their API works is that if a record was successful, then the ID of that record is returned. As such, if that value is there, then we return the record, otherwise, a custom error is thrown.

In the backend folder of the provided repo, feel free to see how the datasources and listApplicants resolvers are defined.

With our schema defined, datasources explained, and resolvers walked through, let’s move over to the frontend to see how we can bring together our fullstack application.

Simple forms with Amplify Studio Form Builder

Amplify UI is a component library that allows frontend applications to quickly build out their UIs with consistency, accessibility, and customization in mind.

Recently, Amplify Studio launched a Form Builder as part of their application sandbox that makes use of Amplify UI components. This means without even needing an AWS account, users can create forms using a drag-and-drop interface. When done creating your form, a downloadable zip file will be provided that contains the UI components needed for your react application.

To get started create a blank form by visiting the AWS Amplify Form Builder.

🔥 pro-tip: Bookmark this site!

form-builder1

To get started, you can create a form as shown above. Note that we are able to drag fields side-by-side by simply selecting the item we’d like to use and moving it. In the above screenshot, I moved the Start Date field to be next to the Employment Status field.

A great aspect of the Form Builder experience is the fields being able to map to our API. By clicking a field and assigning the Data label on the right-panel to the name of a field in our schema, the values in our form will automatically match our API when submitted. As seen in the screenshot, it’s also possible to have Amplify automate the error-handling of required fields by marking them as required.

form-builder2

When done creating the form, feel free to click on Step 2: Set up a React project, however, the project in this post already comes with a NextJS application that has the components and packages available.

Connecting NextJS to the AWS CDK

By making use of the AWS Amplify JavaScript libraries, our frontend becomes aware of our CDK backend:

// _app.js
import { AmplifyProvider } from '@aws-amplify/ui-react'
import '@aws-amplify/ui-react/styles.css'
import { Amplify } from 'aws-amplify'

//this file is typically stored in src/aws-exports.js
const config = {
    aws_appsync_graphqlEndpoint:'https://YOUR_API_URL/graphql',
	aws_appsync_region: 'your-api-region',
	aws_appsync_authenticationType: 'AWS_API_KEY'
}

Amplify.configure(config)

export default function App({ Component, pageProps }) {
	return (
		<AmplifyProvider>
			<Component {...pageProps} />
		</AmplifyProvider>
	)
}

To take the automation even further, copy the schema.graphql file from the backend to the root of the frontend. We can run the following command to have the Amplify CLI generate our queries and mutation strings:

amplify add codgen --apiId your-api-id

With our code generated, we can combine everything we’ve done so far by calling the API.graphql method. This will take in the input provided by our Amplify-generated form, the mutation string provided by the Amplify CLI, and call our CDK-generated AppSync API, connected with the Amplify JavaScript libraries.

await API.graphql({
	query: addApplicant,
	variables: {
		input: {
			...values,
			employmentStatus: values.employmentStatus.toUpperCase(),
		},
	},
})

Upon inspecting our MongoDB collection, we can verify the applicant successfully saved their data.

mongo-collection

To learn more about deploying the frontend to AWS Amplify Hosting using the AWS CDK, refer to this fullstack guide.

Conclusion

In this post, we discussed how AWS AppSync can leverage a pipeline resolver to connect to MongoDB. In doing so, we saw the role AppSync functions play and how HTTP resolvers can be used to connect to internal services like AWS Secrets Manager, and external APIs such as MongoDB.

We also saw how individual pieces of AWS Amplify can be used in conjunction with the AWS CDK. In particular, the new Form Builder is used to map form properties directly to our API and the Amplify CLI can be used to generate our AppSync queries and mutations.

As a next step, feel free to refer to this project’s repo. In addition, this project can be extended by adding tighter authorization with Amazon Cognito. For tips on how to do so, refer to my previous post on securing AppSync APIs.