Building a serverless REST API with AWS SAM and Fauna
Serverless architectures enable you to build applications that are scalable, highly available, and secure without managing infrastructure. Fauna is a serverless relational database that offers ACID transactions and global replication.
In this post, you learn how to build a simple REST API using the AWS Serverless Application Model (AWS SAM) and Fauna. You create a database in Fauna, configure an access key for your application, and securely store that key in your AWS account. You create a basic serverless application that retrieves that key to connect to Fauna. Finally, you add routes to your application to store and retrieve data from your database.
Pre-requisites
To follow along with this post you must have access to a Fauna account and an AWS account. You can register for a free Fauna account and benefit from Fauna’s free tier while you learn and build. You do not need to provide payment information until you upgrade your plan. You can sign up for an AWS account by following these instructions. Although you must provide a payment method to create an AWS account, AWS also offers a free tier.
Optional - deploy from GitHub
If you plan to deploy the sample application from the command line, you must install the AWS SAM CLI. For instructions, see the section Deploying with AWS SAM CLI in the accompanying GitHub repository.
Configuring a database in Fauna
Open the Fauna dashboard and choose “New Database” to create a new database for your application. Enter aws-http-api as the Database Name, ensure that Pre-populate with demo data is selected, and choose Save.
Once your database is available, select the Security tab and choose New Key to create your first key. Accept the defaults of the current database and Admin for the role, enter Parameter Store as the Key Name, and choose Save to create a new key.
Copy the key’s secret to your clipboard to store in the next step.
Storing secrets in Parameter Store
The key you create in the previous step can perform any action on your database, so you must protect it by storing it securely. The AWS Lambda Operator’s Guide warns against storing secrets and API keys as plain text environment variables. Instead, you should store secrets using either AWS Systems Manager Parameter Store or AWS Secrets Manager. Parameter Store is a simpler and cheaper solution for storing an API key that does not change frequently.
Parameter Store SecureString values allow you to encrypt and store secrets and API keys for use in your application. Open Parameter Store in the AWS Management Console and choose Create parameter. Enter
fauna-secret
as the name of your parameter, select SecureString as the parameter type, and paste the key you copied in the previous step in the Value text box. Choose Create parameter to store your key. Parameter Store creates the fauna-secret parameter and displays it on the My parameters tab.Creating a serverless application on AWS
Note: Instructions for recreating the application on your own using the AWS SAM CLI are provided in the accompanying GitHub repository.
Follow this link to deploy an AWS CloudFormation Stack containing all of the AWS resources in this tutorial. Confirm that the FaunaSecretParameter value matches the name of the parameter you created in the previous step. Select all the check boxes in the section Capabilities and transforms to acknowledge that CloudFormation may take actions on your behalf, and choose Create stack to deploy the template in your account.
Once your stack status changes to CREATE_COMPLETE, select the Outputs tab, copy the value of the ListStoresURL output and open the URL in a new browser tab. You should receive a JSON object containing a list of stores from the sample data in your Fauna database!
Retrieving secrets from Parameter Store
Before you connect to your Fauna database in a Lambda function, you must first initialize the Fauna client. To connect successfully, you must retrieve the decrypted value of the parameter you created in the previous section. The following code retrieves the value of the key from Parameter Store.
const { SSM } = require('aws-sdk');
const ssm = new SSM();
const init = async () => {
// Get the Fauna Server Key from AWS Systems Manager Parameter Store at runtime.
const { Parameter: { Value } } = await ssm.getParameter({ Name: process.env.FAUNA_SECRET_PARAMETER, WithDecryption: true }).promise();
// Now 'Value' has the decrypted value of your server key
...
}
// This starts our initialization before a handler is invoked by calling the `init` function above
const initComplete = init();
exports.list = async () => {
await initComplete;
The final two lines in the previous code are the function declaration and first line of the ListStores Lambda function. The call to
await initComplete
will run once per environment when the environment is created. This minimizes the performance impact of retrieving the key from Parameter Store.Adding routes
List stores
ListStores:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/stores.list
Events:
HttpGetRequest:
Type: HttpApi
Properties:
Method: get
Path: /stores
Policies:
# Needed for logging
- AWSLambdaBasicExecutionRole
# Needed to retrieve our database key
- SSMParameterReadPolicy:
ParameterName: !Ref FaunaSecretParameter
The Events section creates an AWS API Gateway HTTP API and connects a route at /stores that listens for HTTP GET requests. When a request is received, API Gateway invokes the ListStores function.
The Handler property tells AWS SAM where to find the source code for the ListStores function. In this case, it looks in the directory src/handlers for a file stores.js that exports a function list.
exports.list = async () => {
await initComplete;
let results = await client.query(
q.Map(
q.Paginate(q.Documents(q.Collection('stores'))),
q.Lambda(product => q.Get(product))
)
);
const response = {
statusCode: 200,
body: JSON.stringify(results)
};
return response;
}
On the first invocation, this function waits until the init function retrieves the Fauna key from Parameter Store and creates a Fauna client using the key. On subsequent invocations the client is already initialized and available. The function then sends a Fauna Query Language (FQL) query via the client. This query maps over all of the documents in the collection stores getting detailed data for each store.
Test the endpoint by sending an HTTP GET request to the ListStoresURL output from your CloudFormation stack using curl, httpie, or an API client like Postman.
curl ${ListStoresURL}
Create store
CreateStore:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/stores.create
Events:
HttpPostRequest:
Type: HttpApi
Properties:
Method: post
Path: /stores
...
The function definition is similar to ListStores. However, API Gateway passes the HTTP request body and other information into the CreateStore function via the event object. CreateFunction creates a new store using the request body as the store’s data.
exports.create = async (event) => {
await initComplete;
let result = await client.query(
q.Create(q.Collection('stores'), { data: JSON.parse(event.body) })
);
Test the endpoint by sending the contents of create-store.json in an HTTP POST request to the CreateStoresURL output from your CloudFormation stack.
curl --request POST \
--header "Content-Type: application/json" \
--data @events/create-store.json \
${CreateStoresURL}
Return to the Fauna dashboard and confirm that you have created a new store. Copy this store’s
id
from the ref
object to use in the next steps.Update store
The following lines define the UpdateStore function in the AWS SAM template. The UpdateStore function requires a path parameter
{id}
to identify which store the database should update.UpdateStore:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/stores.update
Events:
HttpPutRequest:
Type: HttpApi
Properties:
Method: put
Path: /stores/{id}
...
API Gateway passes all path parameters in the event object at
event.pathParameters
. The following code retrieves the {id}
path parameter and sends it in an FQL update query along with the body of the event.exports.update = async (event) => {
await initComplete;
let result = await client.query(
q.Update(
q.Ref(q.Collection('stores'), event.pathParameters.id),
{ data: JSON.parse(event.body) }
)
);
Test the endpoint by sending the contents of update-store.json in an HTTP PUT request to the UpdateStoresURL output from your CloudFormation stack. Note that you must replace
{id}
in the UpdateStoresURL with the value of id
that the CreateStores function returns.curl --request PUT \
--header "Content-Type: application/json" \
--data @events/update-store.json \
${UpdateStoresURL}
Return to the Fauna dashboard and confirm that the store you created has been updated. Note that the value of the name field changes, but the value of the address object remains the same. If you want to remove the address object, you must set it equal to
null
in an update query. For example, you can send the following object to remove the address object without modifying the name field.curl --request PUT \
--header "Content-Type: application/json" \
--data '{ "address": null }' \
${UpdateStoresURL}
Delete store
The following lines define the DeleteStore function in the AWS SAM template. The DeleteStore function also requires a path parameter
{id}
to identify which store the database should delete.DeleteStore:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/stores.delete
Events:
HttpDeleteRequest:
Type: HttpApi
Properties:
Method: delete
Path: /stores/{id}
...
The function definition is similar to UpdateStore, but with a simpler FQL delete query.
exports.delete = async (event) => {
await initComplete;
let result = await client.query(
q.Delete(q.Ref(q.Collection('stores'), event.pathParameters.id)));
Test the endpoint by sending an HTTP DELETE request to the DeleteStoresURL output from your CloudFormation stack. Note again that you must replace
{id}
in the DeleteStoresURL with the value of id
that the CreateStores function returns.curl --request DELETE ${DeleteStoresURL}
Return to the Fauna dashboard and confirm that the store you created has been removed.
Now you have a complete CRUD API that allows you to create, retrieve, update, and delete stores in your Fauna database! Since the application and database are all serverless, your API, functions, and data are highly available and scale to meet demand without any additional configuration or operations.
Next steps
This application demonstrates how to quickly deploy an API with Fauna, AWS Lambda, and Amazon API Gateway. However, before running an API in production you should add authentication, monitoring, input validation, and more. This blog post helps you choose an authentication strategy with Fauna and provides links to example code for first and third-party authentication strategies. The AWS Lambda Operator’s Guide has a complete section on Monitoring and observability for your serverless applications. Finally, for a deeper understanding of FQL, see this blog series and the FQL API documentation.
Cleaning up resources
Once you complete this tutorial, you may wish to remove all the resources you create to avoid unexpected charges. You will need to remove your CloudFormation stack, Parameter Store parameter, CloudWatch logs, and Fauna database.
- Open the CloudFormation console, select the Fauna-HTTP-API stack, and choose Delete. Confirm that you want to delete the stack and all resources by choosing Delete stack. CloudFormation initiates the delete process, which should take less than one minute to complete.
- Open the Parameter Store console, select the
fauna-secret
parameter, and choose Delete. Confirm that you want to delete the parameter by choosing Delete parameters. - Open the CloudWatch console, select Log groups in the Logs section, and select any log groups created by your application that you wish to remove. Open the Actions dropdown menu and choose Delete log group(s). Confirm that you want to delete the selected log groups by choosing Delete.
- Open the Fauna dashboard and choose the
aws-http-api
database from the list of databases. Choose Settings to open the Database Settings screen, then choose Delete. Confirm that you want to delete the database by choosing Delete.
Conclusion
In this post, you learned how to build a simple REST API using AWS SAM and Fauna. You created a database and access key in Fauna and stored that key securely using Parameter Store. You deployed or created a serverless application that retrieves that key to connect to Fauna, and added routes to your application to store and retrieve data from your database.
Deploy this application to your AWS account and start building with Fauna today!
If you enjoyed our blog, and want to work on systems and challenges related to globally distributed systems, serverless databases, GraphQL, and Jamstack, Fauna is hiring!
Subscribe to Fauna's newsletter
Get latest blog posts, development tips & tricks, and latest learning material delivered right to your inbox.