Create a REST API integrated with Amazon DynamoDB using AWS Amplify and Vue

Refresh your REST APIs skills using AWS Amplify and Vue

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.

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

Creating a REST API with Amplify CLI

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

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

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)

// 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 todosapp.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.stringifyas 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. Just note the highlighted parts on the code for important changes.

Fetching a todo by id (GET, /todos/:id)

// client request: fetching a todo by idimport { 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 idapp.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 the Key.sk.

Creating a new todo (POST, /todos)

// client request: creating a new todoimport { 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.bodycontains the description for the new todo: text. Generate a new id, set complete to false, addcreatedAtand 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 functionconst 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)

// client request: updating a todoimport { 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 todoapp.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 the request.params.id.

Deleting a todo by id (DELETE, /todos/:id)

// client request: deleting a todo by idimport { 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 idapp.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

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

git init
git remote add origin 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

amplify delete

Conclusion

If you prefer, you can also follow this video to achieve the same result.

Thanks for reading!

My Name is Gerard Sans. I am a Developer Advocate at AWS Mobile working with AWS Amplify and AWS AppSync teams.