In this article you will create a REST API integrated with Amazon DynamoDB using AWS Amplify including CRUD operations and publication. Access to the REST API will allow both registered users and guests. In order to test it you will create a client including an authentication flow using Vue.
- Setting up a new project with the Vue CLI
- Creating a REST API with Amplify CLI
- Creating a new REST API
- Implementing CRUD operations using Amazon DynamoDB
- Pushing your REST API to the cloud
- Publishing your app via the AWS Amplify Console
- Cleaning up cloud services
Please let me know if you have any questions or want to learn more at @gerardsans.
> Final solution in GitHub.
Setting up a new project with the Vue CLI
Before moving to the next section, please complete the steps described in “Build your first full-stack serverless app with Vue”. Here you will set up the initial project, familiarise with Amplify CLI and add an authorisation flow so users can register themselves via an automated verification code sent to their email and login.
Creating a REST API with Amplify CLI
The Amplify CLI provides a guided workflow to easily add, develop, test and manage REST APIs to access your AWS resources from your web and mobile applications.
A REST API or HTTP endpoint will be composed by one or more paths. Eg: /todos
. Each path will use a Lambda function to handle HTTP requests and responses. Amplify CLI creates a single resource in Amazon API Gateway so you can handle all routes, HTTP Methods and paths, with a single Lambda function via a Lambda Proxy integration. HTTP proxy integrations forward all requests and responses directly through to your HTTP endpoint. Eg: /todos
.
Architecture diagram for the Todo App
Creating a new REST API
In this post, you are going to create a REST API to service a todo app with a single endpoint /todos
using Amazon DynamoDB as a data source. To create it, use the following command:
amplify add api
Answer the following questions
- Please select from one of the below mentioned services: REST
- Provide a friendly name for your resource to be used as a label for this category in the project: todosApi
- Provide a path (e.g., /book/{isbn}): /todos
This will be the configuration for /todos
path in Amazon API Gateway:
/
|\_ /todos Main resource. Eg: /todos
ANY Methods: DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT
OPTIONS Allow pre-flight requests in CORS by browser
|\_ /{proxy+} Eg: /todos/, /todos/id
ANY Methods: DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT
OPTIONS Allow pre-flight requests in CORS by browser
By default, Amplify CLI creates a greedy path variable /todos/{proxy+}
that catches all child resources for a path and forwards them to your Lambda. This will match all child routes including /todos/id
.
- Choose a Lambda source Create a new Lambda function
- Provide a friendly name for your resource to be used as a label for this category in the project: todosLambda
- Provide the AWS Lambda function name: todosLambda
- Choose the runtime that you want to use: NodeJS
- Choose the function template that you want to use: CRUD function for Amazon DynamoDB
The Lambda function template, CRUD function for Amazon DynamoDB implements route handlers for GET
, POST
, PUT
and DELETE
Http Methods and paths for /todos
and /todos/*
. Some possible routes examples include:
GET /todos List all todos
GET /todos/1 Load a todo by id
POST /todos Create a todo
PUT /todos Update a todo
DELETE /todos/1 Delete a todo by id
- Do you want to access other resources in this project from your Lambda function? No
- Do you want to invoke this function on a recurring schedule? No
- Do you want to configure Lambda layers for this function? No
- Do you want to edit the local lambda function now? Yes
We are going to change this template later but it’s good that you have it open as you follow the next steps.
- Press enter to continue
- Restrict API access Yes
- Who should have access? Authenticated and Guest users
- What kind of access do you want for Authenticated users? create, read, update, delete
- What kind of access do you want for Guest users? read
Amplify CLI restricts API access combining Amazon Cognito for authentication and AWS IAM (Identity and Access Management) for granting execution permissions on routes.
- Do you want to add another path? No
That’s all! Before we publish to the cloud though, let’s see how to change the default template to implement create, read, update and delete operations for your todo app.
Implementing CRUD operations with Amazon DynamoDB
In order to manage your todo app you want to implement all CRUD operations, these are: create, read, update and delete. The template we picked in the last section uses AWS Serverless Express. Amazon API Gateway will proxy incoming requests to your todosLambda.
For the implementation, you need to add route handlers to match all HTTP methods and paths you want to support. The first to match with an incoming request will be then executed. This will be in the same order as you have define them. If there’s no match an error will be returned.
See below, how different HTTP methods and operations match route definitions in AWS Serverless Express and DynamoDB Document Client.
REST API mapping between HTTP requests, AWS Serverless Express and Document Client calls.
Before we start, you need to install a library to generate the ids for new todos. To install this dependency run the following commands from the root of your project:
cd /amplify/backend/function/todosLambda/src
npm i --save uuid
Open /amplify/backend/function/todosLambda/src/app.js
and replace its content for:
const AWS = require('aws-sdk')
var awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
var bodyParser = require('body-parser')
var express = require('express')
const { v4: uuidv4 } = require('uuid')
AWS.config.update({ region: process.env.TABLE_REGION });
const dynamodb = new AWS.DynamoDB.DocumentClient();
let tableName = "todosTable";
if (process.env.ENV && process.env.ENV !== "NONE") {
tableName = tableName + '-' + process.env.ENV;
}
var app = express()
app.use(bodyParser.json())
app.use(awsServerlessExpressMiddleware.eventContext())app.use(function (request, response, next) {
response.header("Access-Control-Allow-Origin", "\*")
response.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
next()
});
This is the same code from the default template adding the uuid library. This takes care of initialising DynamoDB Document Client API to interact with DynamoDB, the DynamoDB table name taking into account the current environment and enabling CORS headers.
Let’s handle the first route, we will use the HTTP Method and path by the title for reference as in the next section. For each operation, first we will include the code from the client; and then, followed by the code serving it in the todosLambda.
Fetching todos (GET, /todos)
In the client, we use the Amplify JavaScript library aws-amplify
to consume the REST API. In order to fetch the todos useAPI.get
with the todosApi
followed by the path /todos
and an empty payload. The result is an array with the todos as part of the body
. Remember to use JSON.parse
as the result.body
needs to be in plain text due to HTTP transport. The result object also contains the statusCode
and path as url
.
// client request: fetching todos
import { API } from 'aws-amplify';
API.get('todosApi', '/todos', {}).then(result => {
this.todos = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})
Let’s look at the code in todosLambda. To fetch todos, map a handler function to the GET Method and the /todos
path. Then run a dynamodb.scan
operation. This fetches all records in the table specified. Limit the results to 100 items. Find more details in the Developer Guide from Amazon DynamoDB.
Scan returns all the data in a table so you may consider to change it for a query if you expect more than just few records.
// todosLambda route handler: fetching todos
app.get("/todos", function (request, response) {
let params = {
TableName: tableName,
limit: 100
}
dynamodb.scan(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result.Items) })
}
});
});
This returns the todos array as part of the body
. Don’t forget to use JSON.stringify
as it needs to be in plain text due to HTTP transport.
Once the operation finishes, check for errors and create a JSON response accordingly. You will use the same approach in the rest of operations.
Fetching a todo by id (GET, /todos/:id)
Let’s quickly see the code necessary on the client below.
// client request: fetching a todo by id
import { API } from 'aws-amplify';
API.get('todosApi', `/todos/${id}`, {}).then((result) => {
this.todo = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})
Notice how to use API.get
with a path using a template string generating the path. Eg: /todos/${id}
becomes "/todos/c71f0be2–607d-48bc-b721–4cb77c90a58f"
. The result receives the todo details.
Let’s see how to get the details from a single todo in todosLambda. In order to capture the todo id
use route parameters, a feature from AWS Serverless Express. Route parameters use a semicolon to capture values at specific position in the URL like in /users/**:userId**/books/**:bookId**
. Values are then available in the request object as request.params.userId
and request.params.bookId
.
// todosLambda route handler: fetching a todo by id
app.get("/todos/:id", function (request, response) {
let params = {
TableName: tableName,
Key: {
id: request.params.id
}
}
dynamodb.get(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result.Item) })
}
});
});
As in the code above, use dynamodb.get
to set your table and partition key Key.id
from the request parameters.
For
todosApi
we only have a partition key, if you have a composed key (partition key + sort key) include the sort key too as part of theKey.sk
.
Creating a new todo (POST, /todos)
In order to create a new todo, use API.post
and set the payload to include the new todo description text
within the request body
.
// client request: creating a new todo
import { API } from 'aws-amplify';
API.post('todosApi', '/todos', {
body: {
text: "todo-1"
}
}).then(result => {
this.todo = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})
In todosLambda, creating a new todo is a bit more elaborated as we need to provide some new attributes and values. As we saw in the code above, the request.body
contains the description for the new todo: text
. Generate a new id
, set complete
to false
, addcreatedAt
and updatedAt
timestamps and include the userId
. To create the todo use dynamodb.put
.
// todosLambda route handler: creating a new todoapp.post("/todos", function (request, response) {
const timestamp = new Date().toISOString();
let params = {
TableName: tableName,
Item: {
...request.body,
id: uuidv4(), // auto-generate id
complete: false, // default for new todos
createdAt: timestamp,
updatedAt: timestamp,
userId: getUserId(request) // userId from request
}
}
dynamodb.put(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message, url: request.url });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(params.Item) })
}
});
});
The helper function getUserId
below extracts the user id from Amazon Cognito.
// todosLambda route handler: helper function
const getUserId = (request) => {
try {
const reqContext = request.apiGateway.event.requestContext;
const authProvider = reqContext.identity.cognitoAuthenticationProvider;
return authProvider ? authProvider.split(":CognitoSignIn:").pop() : "UNAUTH";
} catch (error) {
return "UNAUTH";
}
}
This code tries to capture the user id from the request identity context and if unsuccessful, returns an unauthorised token with UNAUTH
as the value.
Updating a todo (PUT, /todos)
At this point you should know how to use the Amplify API. In order to update a todo use API.put
and the /todos
path. As you did creating a new todo provide the todo changes including its id
as part of the body
. As in the code below, change both the description
and the complete
values. The result contains any fields that were changed and their new values.
// client request: updating a todo
import { API } from 'aws-amplify';
API.put('todosApi', `/todos`, {
body: {
id: id,
text: "todo-2",
complete: true
}
}).then(result => {
this.todo = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})
The update in todosLambda uses most of the code you are already familiar but including a new update expression. This expression includes optional attributes so we need to create it dynamically. Eg: to service a request changing only the description text
but not the other attributes. We used UPDATED_NEW
to return only the updated attributes in DynamoDB but if you need all attributes use ALL_NEW
.
// todosLambda route handler: updating a todo
app.put("/todos", function (request, response) {
const timestamp = new Date().toISOString();
const params = {
TableName: tableName,
Key: {
id: request.body.id,
},
ExpressionAttributeNames: { '#text': 'text' },
ExpressionAttributeValues: {},
ReturnValues: 'UPDATED_NEW',
};
params.UpdateExpression = 'SET ';
if (request.body.text) {
params.ExpressionAttributeValues[':text'] = request.body.text;
params.UpdateExpression += '#text = :text, ';
}
if (request.body.complete) {
params.ExpressionAttributeValues[':complete'] = request.body.complete;
params.UpdateExpression += 'complete = :complete, ';
}
if (request.body.text || request.body.complete) {
params.ExpressionAttributeValues[':updatedAt'] = timestamp;
params.UpdateExpression += 'updatedAt = :updatedAt';
}
dynamodb.update(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message, url: request.url });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result.Attributes) })
}
});
});
To keep the semantics of HTTP PUT use the todo id from
request.body.id
instead of therequest.params.id
.
Deleting a todo by id (DELETE, /todos/:id)
Let’s implement the request to delete a todo. On the client, use API.del
with a path including the id
as a route parameter. If successful, the result body
will be {}
with statusCode:200
.
// client request: deleting a todo by id
import { API } from 'aws-amplify';
API.del('todosApi', `/todos/${id}`, {}).then(result => {
console.log(result);
}).catch(err => {
console.log(err);
})
In todosLambda, use the same techniques from the previous route handlers now using dynamodb.delete
operation.
// todosLambda route handler: deleting a todo by id
app.delete("/todos/:id", function (request, response) {
let params = {
TableName: tableName,
Key: {
id: request.params.id
}
}
dynamodb.delete(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message, url: request.url });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result) })
}
});
});
This completes all the CRUD operations. Before we can test it we need to publish it.
Pushing your new REST API to the cloud
Let’s deploy the new REST API in the cloud by running this command in the root of your project:
amplify push
At the end of this command you can take note of your new REST API url.
REST APIs follow this pattern
https://{restapi-id}.execute-api.{region}.amazonaws.com/{environment}/{path}
Let’s see an overview of all the resources created by Amplify CLI.
REST
|\_ /todos (path)
|\_ todosApi (Amazon API Gateway)
|\_ todosLambda (AWS Lambda)
|\_ Logs (Amazon CloudWatch)
We have covered all AWS Services but Amazon CloudWatch. This service will allow you to monitor usage and access to logs during development and testing.
Publishing your app via the AWS Amplify Console
The first thing you need to do is create a new repo for this project. Once you’ve created the repo, copy the URL for the project to the clipboard and initialise git in your local project:
git init
git remote add origin [repo@repoofyourchoice.com](mailto:repo@repoofyourchoice.com):username/project-name.git
git add .git commit -m 'initial commit'git push origin master
Next visit the AWS Amplify Console in your AWS account. Click Get Started to create a new deployment. Next, authorise your repository provider as the repository service. Next, choose the new repository and branch for the project you just created and click Next. In the next screen, create a new role and use this role to allow the AWS Amplify Console to deploy these resources and click Next. Finally, click Save and Deploy to deploy your application!
AWS Amplify Console deployment steps.
Cleaning up cloud services
If at any time, you would like to delete the services from your project and your AWS Account, you can do this by running:
amplify delete
Conclusion
Congratulations! You successfully created a REST API using AWS Amplify, implemented all CRUD operations using Amazon DynamoDB and created a client to consume it restricting access to both registered users and guests using Vue. Thanks for following this tutorial.
If you prefer, you can also follow this video to achieve the same result.
Thanks for reading!
Have you got any questions about this tutorial or AWS Amplify? Feel free to reach me anytime at @gerardsans.
My Name is Gerard Sans. I am a Developer Advocate at AWS Mobile working with AWS Amplify and AWS AppSync teams.
Top comments (0)