Skip to content

Commit 4221402

Browse files
committed
feat: add idempontent example for aws node express
1 parent 3c8a2e3 commit 4221402

File tree

6 files changed

+301
-0
lines changed

6 files changed

+301
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
.serverless
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
<!--
2+
title: 'Serverless Framework Node Express API service backed by DynamoDB on AWS with Idempotence Guaranteed'
3+
description: 'This template demonstrates how to develop and deploy a simple Node Express API service backed by DynamoDB running on AWS Lambda using the traditional Serverless Framework.'
4+
layout: Doc
5+
framework: v2
6+
platform: AWS
7+
language: nodeJS
8+
priority: 1
9+
authorLink: 'https://github.com/serverless'
10+
authorName: 'Serverless, inc.'
11+
authorAvatar: 'https://avatars1.githubusercontent.com/u/13742415?s=200&v=4'
12+
-->
13+
14+
# Serverless Framework Node Express API on AWS with Idempotence Guaranteed
15+
16+
This template demonstrates how to add idempotence in a simple Node Express API service, backed by DynamoDB database, running on AWS Lambda using the traditional Serverless Framework. It is based on the example of [Serverless Framework Node Express API on AWS](../aws-node-express-dynamodb-api/README.md).
17+
18+
## Anatomy of the template
19+
20+
This template configures a single function, `api`, which is responsible for handling all incoming requests thanks to the `httpApi` event. To learn more about `httpApi` event configuration options, please refer to [httpApi event docs](https://www.serverless.com/framework/docs/providers/aws/events/http-api/). As the event is configured in a way to accept all incoming requests, `express` framework is responsible for routing and handling requests internally. Implementation takes advantage of `serverless-http` package, which allows you to wrap existing `express` applications. To learn more about `serverless-http`, please refer to corresponding [GitHub repository](https://github.com/dougmoscrop/serverless-http). Additionally, it also handles provisioning of a DynamoDB database that is used for storing data about users. It uses context from the request as the `clientRequestToken` parameter in the [transactional write api](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html) of DynamoDB to enable the guarantee of idempotence. The `express` application exposes two endpoints, `POST /users` and `GET /user/{userId}`, which allow to create and retrieve users.
21+
22+
## Usage
23+
24+
### Deployment
25+
26+
Install dependencies with:
27+
28+
```
29+
npm install
30+
```
31+
32+
and then deploy with:
33+
34+
```
35+
serverless deploy
36+
```
37+
38+
After running deploy, you should see output similar to:
39+
40+
```bash
41+
Serverless: Packaging service...
42+
Serverless: Excluding development dependencies...
43+
Serverless: Creating Stack...
44+
Serverless: Checking Stack create progress...
45+
........
46+
Serverless: Stack create finished...
47+
Serverless: Uploading CloudFormation file to S3...
48+
Serverless: Uploading artifacts...
49+
Serverless: Uploading service aws-node-express-dynamodb-api.zip file to S3 (718.53 KB)...
50+
Serverless: Validating template...
51+
Serverless: Updating Stack...
52+
Serverless: Checking Stack update progress...
53+
....................................
54+
Serverless: Stack update finished...
55+
Service Information
56+
service: aws-node-express-dynamodb-api
57+
stage: dev
58+
region: us-east-1
59+
stack: aws-node-express-dynamodb-api-dev
60+
resources: 13
61+
api keys:
62+
None
63+
endpoints:
64+
ANY - https://xxxxxxx.execute-api.us-east-1.amazonaws.com/
65+
functions:
66+
api: aws-node-express-dynamodb-api-dev-api
67+
layers:
68+
None
69+
```
70+
71+
_Note_: In current form, after deployment, your API is public and can be invoked by anyone. For production deployments, you might want to configure an authorizer. For details on how to do that, refer to [`httpApi` event docs](https://www.serverless.com/framework/docs/providers/aws/events/http-api/). Additionally, in current configuration, the DynamoDB table will be removed when running `serverless remove`. To retain the DynamoDB table even after removal of the stack, add `DeletionPolicy: Retain` to its resource definition.
72+
73+
### Invocation
74+
75+
After successful deployment, you can create a new user by calling the corresponding endpoint:
76+
77+
```bash
78+
curl --request POST 'https://xxxxxx.execute-api.us-east-1.amazonaws.com/users' --header 'Content-Type: application/json' --data-raw '{"name": "John", "userId": "someUserId"}'
79+
```
80+
81+
Which should result in the following response:
82+
83+
```bash
84+
{"userId":"someUserId","name":"John"}
85+
```
86+
87+
You can later retrieve the user by `userId` by calling the following endpoint:
88+
89+
```bash
90+
curl https://xxxxxxx.execute-api.us-east-1.amazonaws.com/users/someUserId
91+
```
92+
93+
Which should result in the following response:
94+
95+
```bash
96+
{"userId":"someUserId","name":"John"}
97+
```
98+
99+
If you try to retrieve user that does not exist, you should receive the following response:
100+
101+
```bash
102+
{"error":"Could not find user with provided \"userId\""}
103+
```
104+
105+
### Local development
106+
107+
It is also possible to emulate DynamoDB, API Gateway and Lambda locally using the `serverless-dynamodb-local` and `serverless-offline` plugins. In order to do that, run:
108+
109+
```bash
110+
serverless plugin install -n serverless-dynamodb-local
111+
serverless plugin install -n serverless-offline
112+
```
113+
114+
It will add both plugins to `devDependencies` in `package.json` file as well as will add it to `plugins` in `serverless.yml`. Make sure that `serverless-offline` is listed as last plugin in `plugins` section:
115+
116+
```
117+
plugins:
118+
- serverless-dynamodb-local
119+
- serverless-offline
120+
```
121+
122+
You should also add the following config to `custom` section in `serverless.yml`:
123+
124+
```
125+
custom:
126+
(...)
127+
dynamodb:
128+
start:
129+
migrate: true
130+
stages:
131+
- dev
132+
```
133+
134+
Additionally, we need to reconfigure `AWS.DynamoDB.DocumentClient` to connect to our local instance of DynamoDB. We can take advantage of `IS_OFFLINE` environment variable set by `serverless-offline` plugin and replace:
135+
136+
```javascript
137+
const dynamoDbClient = new AWS.DynamoDB.DocumentClient();
138+
```
139+
140+
with the following:
141+
142+
```javascript
143+
const dynamoDbClientParams = {};
144+
if (process.env.IS_OFFLINE) {
145+
dynamoDbClientParams.region = 'localhost'
146+
dynamoDbClientParams.endpoint = 'http://localhost:8000'
147+
}
148+
const dynamoDbClient = new AWS.DynamoDB.DocumentClient(dynamoDbClientParams);
149+
```
150+
151+
After that, running the following command with start both local API Gateway emulator as well as local instance of emulated DynamoDB:
152+
153+
```bash
154+
serverless offline start
155+
```
156+
157+
To learn more about the capabilities of `serverless-offline` and `serverless-dynamodb-local`, please refer to their corresponding GitHub repositories:
158+
- https://github.com/dherault/serverless-offline
159+
- https://github.com/99x/serverless-dynamodb-local
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const AWS = require("aws-sdk");
2+
const express = require("express");
3+
const serverless = require("serverless-http");
4+
5+
const app = express();
6+
7+
const USERS_TABLE = process.env.USERS_TABLE;
8+
const dynamoDbClient = new AWS.DynamoDB.DocumentClient();
9+
10+
app.use(express.json());
11+
12+
app.get("/users/:userId", async function (req, res) {
13+
const params = {
14+
TableName: USERS_TABLE,
15+
Key: {
16+
userId: req.params.userId,
17+
},
18+
};
19+
20+
try {
21+
const { Item } = await dynamoDbClient.get(params).promise();
22+
if (Item) {
23+
const { userId, name } = Item;
24+
res.json({ userId, name });
25+
} else {
26+
res
27+
.status(404)
28+
.json({ error: 'Could not find user with provided "userId"' });
29+
}
30+
} catch (error) {
31+
console.log(error);
32+
res.status(500).json({ error: "Could not retreive user" });
33+
}
34+
});
35+
36+
app.post("/users", async function (req, res) {
37+
const { userId, name } = req.body;
38+
if (typeof userId !== "string") {
39+
res.status(400).json({ error: '"userId" must be a string' });
40+
} else if (typeof name !== "string") {
41+
res.status(400).json({ error: '"name" must be a string' });
42+
}
43+
44+
const params = {
45+
ClientRequestToken: req.context.awsRequestId,
46+
TransactItems: [
47+
{
48+
Update: {
49+
TableName: USERS_TABLE,
50+
Key: { userId: userId },
51+
UpdateExpression: 'set #a = :v',
52+
ExpressionAttributeNames: {'#a' : 'name'},
53+
ExpressionAttributeValues: {
54+
':v': name
55+
}
56+
}
57+
}
58+
]
59+
};
60+
try {
61+
await dynamoDbClient.transactWrite(params).promise();
62+
res.json({ userId, name });
63+
} catch (error) {
64+
console.log(error);
65+
res.status(500).json({ error: "Could not create user" });
66+
}
67+
});
68+
69+
app.use((req, res, next) => {
70+
return res.status(404).json({
71+
error: "Not Found",
72+
});
73+
});
74+
75+
76+
module.exports.handler = serverless(app,{
77+
request: function(req, _event, context) {
78+
req.context = context;
79+
}
80+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "aws-node-express-dynamodb-api",
3+
"version": "1.0.0",
4+
"description": "",
5+
"dependencies": {
6+
"express": "^4.17.1",
7+
"serverless-http": "^2.7.0"
8+
}
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: aws-node-express-dynamodb-api
2+
org: serverlessinc
3+
description: Deploys a Node Express API service backed by DynamoDB with Serverless Framework
4+
keywords: aws, serverless, faas, lambda, node, express, dynamodb
5+
repo: https://github.com/serverless/examples/aws-node-express-dynamodb-api
6+
license: MIT
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
service: aws-node-express-dynamodb-api
2+
frameworkVersion: '2'
3+
4+
custom:
5+
tableName: 'users-table-${sls:stage}'
6+
7+
provider:
8+
name: aws
9+
runtime: nodejs12.x
10+
lambdaHashingVersion: '20201221'
11+
iam:
12+
role:
13+
statements:
14+
- Effect: Allow
15+
Action:
16+
- dynamodb:Query
17+
- dynamodb:Scan
18+
- dynamodb:GetItem
19+
- dynamodb:PutItem
20+
- dynamodb:UpdateItem
21+
- dynamodb:DeleteItem
22+
Resource:
23+
- Fn::GetAtt: [ UsersTable, Arn ]
24+
environment:
25+
USERS_TABLE: ${self:custom.tableName}
26+
27+
functions:
28+
api:
29+
handler: handler.handler
30+
events:
31+
- httpApi: '*'
32+
33+
resources:
34+
Resources:
35+
UsersTable:
36+
Type: AWS::DynamoDB::Table
37+
Properties:
38+
AttributeDefinitions:
39+
- AttributeName: userId
40+
AttributeType: S
41+
KeySchema:
42+
- AttributeName: userId
43+
KeyType: HASH
44+
BillingMode: PAY_PER_REQUEST
45+
TableName: ${self:custom.tableName}

0 commit comments

Comments
 (0)