Skip to content

Commit 4243b4e

Browse files
simalexanrix0rrr
authored andcommitted
Add an example for API Gateway with CORS, and CRUD Lambdas with DynamoDB (aws-samples#38)
1 parent 2420477 commit 4243b4e

File tree

11 files changed

+384
-0
lines changed

11 files changed

+384
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ $ cdk destroy
2020

2121
| Example | Description |
2222
|---------|-------------|
23+
| [api-cors-lambda-crud-dynamodb](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/api-cors-lambda-crud-dynamodb/) | Creating a single API with CORS, and five Lambdas doing CRUD operations over a single DynamoDB |
2324
| [application-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/application-load-balancer/) | Using an AutoScalingGroup with an Application Load Balancer |
2425
| [classic-load-balancer](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/classic-load-balancer/) | Using an AutoScalingGroup with a Classic Load Balancer |
2526
| [custom-resource](https://github.com/aws-samples/aws-cdk-examples/tree/master/typescript/custom-resource/) | Shows adding a Custom Resource to your CDK app |
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# API GW with CORS five Lambdas CRUD with DynamoDB
2+
3+
This an example of an API GW with CORS enabled, pointing to five Lambdas doing CRUD operations with a single DynamoDB table.
4+
5+
## Build
6+
7+
To build this app, you need to be in this example's root folder. Then run the following:
8+
9+
```bash
10+
npm install -g aws-cdk
11+
npm install
12+
npm run build
13+
```
14+
15+
This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template.
16+
17+
## Deploy
18+
19+
Run `cdk deploy`. This will deploy / redeploy your Stack to your AWS Account.
20+
21+
After the deployment you will see the API's URL, which represents the url you can then use.
22+
23+
## The Component Structure
24+
25+
The whole component contains:
26+
27+
- an API, with CORS enabled on all HTTTP Methods. (Use with caution, for production apps you will want to enable only a certain domain origin to be able to query your API.)
28+
- Lambda pointing to `src/create.ts`, containing code for storing an item into the DynamoDB table.
29+
- Lambda pointing to `src/delete-one.ts`, containing code for deleting an item from the DynamoDB table.
30+
- Lambda pointing to `src/get-all.ts`, containing code for getting all items from the DynamoDB table.
31+
- Lambda pointing to `src/get-one.ts`, containing code for getting an item from the DynamoDB table.
32+
- Lambda pointing to `src/update-one.ts`, containing code for updating an item in the DynamoDB table.
33+
- a DynamoDB table `items` that stores the data.
34+
- five LambdaIntegrations that connect these Lambdas to the API.
35+
36+
## CDK Toolkit
37+
38+
The [`cdk.json`](./cdk.json) file in the root of this repository includes
39+
instructions for the CDK toolkit on how to execute this program.
40+
41+
After building your TypeScript code, you will be able to run the CDK toolkits commands as usual:
42+
43+
$ cdk ls
44+
<list all stacks in this program>
45+
46+
$ cdk synth
47+
<cloudformation template>
48+
49+
$ cdk deploy
50+
<deploy stack to your account>
51+
52+
$ cdk diff
53+
<diff against deployed stack>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"app": "node index"
3+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import apigateway = require('@aws-cdk/aws-apigateway');
2+
import dynamodb = require('@aws-cdk/aws-dynamodb');
3+
import lambda = require('@aws-cdk/aws-lambda');
4+
import cdk = require('@aws-cdk/cdk');
5+
6+
export class ApiLambdaCrudDynamoDBStack extends cdk.Stack {
7+
constructor(app: cdk.App, id: string) {
8+
super(app, id);
9+
10+
const dynamoTable = new dynamodb.Table(this, 'items', {
11+
partitionKey: {
12+
name: 'itemId',
13+
type: dynamodb.AttributeType.String
14+
},
15+
tableName: 'items'
16+
});
17+
18+
const getOneLambda = new lambda.Function(this, 'getOneItemFunction', {
19+
code: new lambda.AssetCode('src'),
20+
handler: 'get-one.handler',
21+
runtime: lambda.Runtime.NodeJS810,
22+
environment: {
23+
TABLE_NAME: dynamoTable.tableName,
24+
PRIMARY_KEY: 'itemId'
25+
}
26+
});
27+
28+
const getAllLambda = new lambda.Function(this, 'getAllItemsFunction', {
29+
code: new lambda.AssetCode('src'),
30+
handler: 'get-all.handler',
31+
runtime: lambda.Runtime.NodeJS810,
32+
environment: {
33+
TABLE_NAME: dynamoTable.tableName,
34+
PRIMARY_KEY: 'itemId'
35+
}
36+
});
37+
38+
const createOne = new lambda.Function(this, 'createItemFunction', {
39+
code: new lambda.AssetCode('src'),
40+
handler: 'create.handler',
41+
runtime: lambda.Runtime.NodeJS810,
42+
environment: {
43+
TABLE_NAME: dynamoTable.tableName,
44+
PRIMARY_KEY: 'itemId'
45+
}
46+
});
47+
48+
const updateOne = new lambda.Function(this, 'updateItemFunction', {
49+
code: new lambda.AssetCode('src'),
50+
handler: 'update-one.handler',
51+
runtime: lambda.Runtime.NodeJS810,
52+
environment: {
53+
TABLE_NAME: dynamoTable.tableName,
54+
PRIMARY_KEY: 'itemId'
55+
}
56+
});
57+
58+
const deleteOne = new lambda.Function(this, 'deleteItemFunction', {
59+
code: new lambda.AssetCode('src'),
60+
handler: 'delete-one.handler',
61+
runtime: lambda.Runtime.NodeJS810,
62+
environment: {
63+
TABLE_NAME: dynamoTable.tableName,
64+
PRIMARY_KEY: 'itemId'
65+
}
66+
});
67+
68+
dynamoTable.grantReadWriteData(getAllLambda);
69+
dynamoTable.grantReadWriteData(getOneLambda);
70+
dynamoTable.grantReadWriteData(createOne);
71+
dynamoTable.grantReadWriteData(updateOne);
72+
dynamoTable.grantReadWriteData(createOne);
73+
dynamoTable.grantReadWriteData(deleteOne);
74+
75+
const api = new apigateway.RestApi(this, 'itemsApi', {
76+
restApiName: 'Items Service'
77+
});
78+
79+
const items = api.root.addResource('items');
80+
const getAllIntegration = new apigateway.LambdaIntegration(getAllLambda);
81+
items.addMethod('GET', getAllIntegration);
82+
83+
const createOneIntegration = new apigateway.LambdaIntegration(createOne);
84+
items.addMethod('POST', createOneIntegration);
85+
addCorsOptions(items);
86+
87+
const singleItem = items.addResource('{id}');
88+
const getOneIntegration = new apigateway.LambdaIntegration(getOneLambda);
89+
singleItem.addMethod('GET', getOneIntegration);
90+
91+
const updateOneIntegration = new apigateway.LambdaIntegration(updateOne);
92+
singleItem.addMethod('PATCH', updateOneIntegration);
93+
94+
const deleteOneIntegration = new apigateway.LambdaIntegration(deleteOne);
95+
singleItem.addMethod('DELETE', deleteOneIntegration);
96+
addCorsOptions(singleItem);
97+
}
98+
}
99+
100+
export function addCorsOptions(apiResource: apigateway.IResource) {
101+
apiResource.addMethod('OPTIONS', new apigateway.MockIntegration({
102+
integrationResponses: [{
103+
statusCode: '200',
104+
responseParameters: {
105+
'method.response.header.Access-Control-Allow-Headers': "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
106+
'method.response.header.Access-Control-Allow-Origin': "'*'",
107+
'method.response.header.Access-Control-Allow-Credentials': "'false'",
108+
'method.response.header.Access-Control-Allow-Methods': "'OPTIONS,GET,PUT,POST,DELETE'",
109+
},
110+
}],
111+
passthroughBehavior: apigateway.PassthroughBehavior.Never,
112+
requestTemplates: {
113+
"application/json": "{\"statusCode\": 200}"
114+
},
115+
}), {
116+
methodResponses: [{
117+
statusCode: '200',
118+
responseParameters: {
119+
'method.response.header.Access-Control-Allow-Headers': true,
120+
'method.response.header.Access-Control-Allow-Methods': true,
121+
'method.response.header.Access-Control-Allow-Credentials': true,
122+
'method.response.header.Access-Control-Allow-Origin': true,
123+
},
124+
}]
125+
})
126+
}
127+
128+
const app = new cdk.App();
129+
new ApiLambdaCrudDynamoDBStack(app, 'ApiLambdaCrudDynamoDBExample');
130+
app.run();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "api-lambda-crud-dynamodb",
3+
"version": "0.23.0",
4+
"description": "Running an API Gateway with four Lambdas to do CRUD operations on DynamoDB",
5+
"private": true,
6+
"scripts": {
7+
"build": "tsc",
8+
"watch": "tsc -w",
9+
"cdk": "cdk"
10+
},
11+
"author": {
12+
"name": "Aleksandar Simovic <[email protected]>",
13+
"url": "https://serverless.pub"
14+
},
15+
"license": "MIT",
16+
"devDependencies": {
17+
"@types/node": "^8.10.38",
18+
"typescript": "^3.2.4"
19+
},
20+
"dependencies": {
21+
"@aws-cdk/aws-apigateway": "*",
22+
"@aws-cdk/aws-dynamodb": "*",
23+
"@aws-cdk/aws-lambda": "*",
24+
"@aws-cdk/cdk": "*"
25+
}
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const AWS = require('aws-sdk');
2+
const db = new AWS.DynamoDB.DocumentClient();
3+
const uuidv4 = require('uuid/v4');
4+
const TABLE_NAME = process.env.TABLE_NAME || '';
5+
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';
6+
7+
const RESERVED_RESPONSE = `Error: You're using AWS reserved keywords as attributes`,
8+
DYNAMODB_EXECUTION_ERROR = `Error: Execution update, caused a Dynamodb error, please take a look at your CloudWatch Logs.`;
9+
10+
export const handler = async (event: any = {}) : Promise <any> => {
11+
12+
if (!event.body) {
13+
return { statusCode: 400, body: 'invalid request, you are missing the parameter body' };
14+
}
15+
const item = typeof event.body == 'object' ? event.body : JSON.parse(event.body);
16+
item[PRIMARY_KEY] = uuidv4();
17+
const params = {
18+
TableName: TABLE_NAME,
19+
Item: item
20+
};
21+
22+
try {
23+
await db.put(params).promise();
24+
return { statusCode: 201, body: '' };
25+
} catch (dbError) {
26+
const errorResponse = dbError.code === 'ValidationException' && dbError.message.includes('reserved keyword') ?
27+
DYNAMODB_EXECUTION_ERROR : RESERVED_RESPONSE;
28+
return { statusCode: 500, body: errorResponse };
29+
}
30+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const AWS = require('aws-sdk');
2+
const db = new AWS.DynamoDB.DocumentClient();
3+
const TABLE_NAME = process.env.TABLE_NAME || '';
4+
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';
5+
6+
export const handler = async (event: any = {}) : Promise <any> => {
7+
8+
const requestedItemId = event.pathParameters.id;
9+
if (!requestedItemId) {
10+
return { statusCode: 400, body: `Error: You are missing the path parameter id` };
11+
}
12+
13+
const params = {
14+
TableName: TABLE_NAME,
15+
Key: {
16+
[PRIMARY_KEY]: requestedItemId
17+
}
18+
};
19+
20+
try {
21+
await db.delete(params).promise();
22+
return { statusCode: 200, body: '' };
23+
} catch (dbError) {
24+
return { statusCode: 500, body: JSON.stringify(dbError) };
25+
}
26+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const AWS = require('aws-sdk');
2+
const db = new AWS.DynamoDB.DocumentClient();
3+
const TABLE_NAME = process.env.TABLE_NAME || '';
4+
5+
export const handler = async () : Promise <any> => {
6+
7+
const params = {
8+
TableName: TABLE_NAME
9+
};
10+
11+
try {
12+
const response = await db.scan(params).promise();
13+
return { statusCode: 200, body: JSON.stringify(response.Items) };
14+
} catch (dbError) {
15+
return { statusCode: 500, body: JSON.stringify(dbError)};
16+
}
17+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const AWS = require('aws-sdk');
2+
const db = new AWS.DynamoDB.DocumentClient();
3+
const TABLE_NAME = process.env.TABLE_NAME || '';
4+
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';
5+
6+
export const handler = async (event: any = {}) : Promise <any> => {
7+
8+
const requestedItemId = event.pathParameters.id;
9+
if (!requestedItemId) {
10+
return { statusCode: 400, body: `Error: You are missing the path parameter id` };
11+
}
12+
13+
const params = {
14+
TableName: TABLE_NAME,
15+
Key: {
16+
[PRIMARY_KEY]: requestedItemId
17+
}
18+
};
19+
20+
try {
21+
const response = await db.get(params).promise();
22+
return { statusCode: 200, body: JSON.stringify(response.Item) };
23+
} catch (dbError) {
24+
return { statusCode: 500, body: JSON.stringify(dbError) };
25+
}
26+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const AWS = require('aws-sdk');
2+
const db = new AWS.DynamoDB.DocumentClient();
3+
const TABLE_NAME = process.env.TABLE_NAME || '';
4+
const PRIMARY_KEY = process.env.PRIMARY_KEY || '';
5+
6+
const RESERVED_RESPONSE = `Error: You're using AWS reserved keywords as attributes`,
7+
DYNAMODB_EXECUTION_ERROR = `Error: Execution update, caused a Dynamodb error, please take a look at your CloudWatch Logs.`;
8+
9+
export const handler = async (event: any = {}) : Promise <any> => {
10+
11+
if (!event.body) {
12+
return { statusCode: 400, body: 'invalid request, you are missing the parameter body' };
13+
}
14+
15+
const editedItemId = event.pathParameters.id;
16+
if (!editedItemId) {
17+
return { statusCode: 400, body: 'invalid request, you are missing the path parameter id' };
18+
}
19+
20+
const editedItem: any = typeof event.body == 'object' ? event.body : JSON.parse(event.body);
21+
const editedItemProperties = Object.keys(editedItem);
22+
if (!editedItem || editedItemProperties.length < 1) {
23+
return { statusCode: 400, body: 'invalid request, no arguments provided' };
24+
}
25+
26+
const firstProperty = editedItemProperties.splice(0,1);
27+
const params: any = {
28+
TableName: TABLE_NAME,
29+
Key: {
30+
[PRIMARY_KEY]: editedItemId
31+
},
32+
UpdateExpression: `set ${firstProperty} = :${firstProperty}`,
33+
ExpressionAttributeValues: {},
34+
ReturnValues: 'UPDATED_NEW'
35+
}
36+
params.ExpressionAttributeValues[`:${firstProperty}`] = editedItem[`${firstProperty}`];
37+
38+
editedItemProperties.forEach(property => {
39+
params.UpdateExpression += `, ${property} = :${property}`;
40+
params.ExpressionAttributeValues[`:${property}`] = editedItem[property];
41+
});
42+
43+
try {
44+
await db.update(params).promise();
45+
return { statusCode: 204, body: '' };
46+
} catch (dbError) {
47+
const errorResponse = dbError.code === 'ValidationException' && dbError.message.includes('reserved keyword') ?
48+
DYNAMODB_EXECUTION_ERROR : RESERVED_RESPONSE;
49+
return { statusCode: 500, body: errorResponse };
50+
}
51+
};

0 commit comments

Comments
 (0)