Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6e9847b

Browse files
lxhunterAlex Jäger
andauthoredMay 3, 2025··
feat: add multiple custom templates (#632)
* feat: add multiple custom templates * fix: indentation * fix: responseParameters were not working as expected --------- Co-authored-by: Alex Jäger <[email protected]>
1 parent 02c6177 commit 6e9847b

File tree

3 files changed

+332
-121
lines changed

3 files changed

+332
-121
lines changed
 

‎README.md

Lines changed: 139 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -9,67 +9,67 @@ Serverless Framework v2.32.0 or later is required.
99

1010
## TOC
1111

12-
- [Install](#install)
13-
- [Setup](#Setup)
14-
- [Adding a custom name for a state machine](#adding-a-custom-name-for-a-statemachine)
15-
- [Adding a custom logical id for a stateMachine](#adding-a-custom-logical-id-for-a-statemachine)
16-
- [Depending on another logical id](#depending-on-another-logical-id)
17-
- [Adding retain property for a state machine](#adding-retain-property-for-a-statemachine)
18-
- [CloudWatch Alarms](#cloudwatch-alarms)
19-
- [CloudWatch Notifications](#cloudwatch-notifications)
20-
- [Blue-Green deployments](#blue-green-deployment)
21-
- [Pre-deployment validation](#pre-deployment-validation)
22-
- [Express Workflow](#express-workflow)
23-
- [CloudWatch Logs](#cloudwatch-logs)
24-
- [X-Ray](#x-ray)
25-
- [Current Gotcha](#current-gotcha)
26-
- [Events](#events)
27-
- [API Gateway](#api-gateway)
28-
- [Simple HTTP endpoint](#simple-http-endpoint)
29-
- [Custom Step Functions Action](#custom-step-functions-action)
30-
- [HTTP Endpoint with custom IAM Role](#http-endpoint-with-custom-iam-role)
31-
- [Share API Gateway and API Resources](#share-api-gateway-and-api-resources)
32-
- [Enabling CORS](#enabling-cors)
33-
- [HTTP Endpoints with AWS_IAM Authorizers](#http-endpoints-with-aws_iam-authorizers)
34-
- [HTTP Endpoints with Custom Authorizers](#http-endpoints-with-custom-authorizers)
35-
- [Shared Authorizer](#shared-authorizer)
36-
- [LAMBDA_PROXY request template](#lambda_proxy-request-template)
37-
- [Customizing request body mapping templates](#customizing-request-body-mapping-templates)
38-
- [Customizing response headers and templates](#customizing-response-headers-and-templates)
39-
- [Send request to an API](#send-request-to-an-api)
40-
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
41-
- [Request Schema Validators](#request-schema-validators)
42-
- [Schedule](#schedule)
43-
- [Enabling / Disabling](#enabling--disabling)
44-
- [Specify Name and Description](#specify-name-and-description)
45-
- [Scheduled Events IAM Role](#scheduled-events-iam-role)
46-
- [Specify InputTransformer](#specify-inputtransformer)
47-
- [Use EventBridge Scheduler instead of EventBridge rules](#use-eventbridge-scheduler-instead-of-eventbridge-rules)
48-
- [CloudWatch Event](#cloudwatch-event)
49-
- [Simple event definition](#simple-event-definition)
50-
- [Enabling / Disabling](#enabling--disabling-1)
51-
- [Specify Input or Inputpath or InputTransformer](#specify-input-or-inputpath-or-inputtransformer)
52-
- [Specifying a Description](#specifying-a-description)
53-
- [Specifying a Name](#specifying-a-name)
54-
- [Specifying a RoleArn](#specifying-a-rolearn)
55-
- [Specifying a custom CloudWatch EventBus](#specifying-a-custom-cloudwatch-eventbus)
56-
- [Specifying a custom EventBridge EventBus](#specifying-a-custom-eventbridge-eventbus)
57-
- [Specifying a DeadLetterQueue](#specifying-a-deadletterqueue)
58-
- [Tags](#tags)
59-
- [Commands](#commands)
60-
- [deploy](#deploy)
61-
- [invoke](#invoke)
62-
- [IAM Role](#iam-role)
63-
- [Tips](#tips)
64-
- [How to specify the stateMachine ARN to environment variables](#how-to-specify-the-statemachine-arn-to-environment-variables)
65-
- [How to split up state machines into files](#how-to-split-up-state-machines-into-files)
66-
- [Sample statemachines setting in serverless.yml](#sample-statemachines-setting-in-serverlessyml)
67-
- [Wait State](#wait-state)
68-
- [Retry Failure](#retry-failure)
69-
- [Parallel](#parallel)
70-
- [Catch Failure](#catch-failure)
71-
- [Choice](#choice)
72-
- [Map](#map)
12+
- [Install](#install)
13+
- [Setup](#Setup)
14+
- [Adding a custom name for a state machine](#adding-a-custom-name-for-a-statemachine)
15+
- [Adding a custom logical id for a stateMachine](#adding-a-custom-logical-id-for-a-statemachine)
16+
- [Depending on another logical id](#depending-on-another-logical-id)
17+
- [Adding retain property for a state machine](#adding-retain-property-for-a-statemachine)
18+
- [CloudWatch Alarms](#cloudwatch-alarms)
19+
- [CloudWatch Notifications](#cloudwatch-notifications)
20+
- [Blue-Green deployments](#blue-green-deployment)
21+
- [Pre-deployment validation](#pre-deployment-validation)
22+
- [Express Workflow](#express-workflow)
23+
- [CloudWatch Logs](#cloudwatch-logs)
24+
- [X-Ray](#x-ray)
25+
- [Current Gotcha](#current-gotcha)
26+
- [Events](#events)
27+
- [API Gateway](#api-gateway)
28+
- [Simple HTTP endpoint](#simple-http-endpoint)
29+
- [Custom Step Functions Action](#custom-step-functions-action)
30+
- [HTTP Endpoint with custom IAM Role](#http-endpoint-with-custom-iam-role)
31+
- [Share API Gateway and API Resources](#share-api-gateway-and-api-resources)
32+
- [Enabling CORS](#enabling-cors)
33+
- [HTTP Endpoints with AWS_IAM Authorizers](#http-endpoints-with-aws_iam-authorizers)
34+
- [HTTP Endpoints with Custom Authorizers](#http-endpoints-with-custom-authorizers)
35+
- [Shared Authorizer](#shared-authorizer)
36+
- [LAMBDA_PROXY request template](#lambda_proxy-request-template)
37+
- [Customizing request body mapping templates](#customizing-request-body-mapping-templates)
38+
- [Customizing response headers and templates](#customizing-response-headers-and-templates)
39+
- [Send request to an API](#send-request-to-an-api)
40+
- [Setting API keys for your Rest API](#setting-api-keys-for-your-rest-api)
41+
- [Request Schema Validators](#request-schema-validators)
42+
- [Schedule](#schedule)
43+
- [Enabling / Disabling](#enabling--disabling)
44+
- [Specify Name and Description](#specify-name-and-description)
45+
- [Scheduled Events IAM Role](#scheduled-events-iam-role)
46+
- [Specify InputTransformer](#specify-inputtransformer)
47+
- [Use EventBridge Scheduler instead of EventBridge rules](#use-eventbridge-scheduler-instead-of-eventbridge-rules)
48+
- [CloudWatch Event](#cloudwatch-event)
49+
- [Simple event definition](#simple-event-definition)
50+
- [Enabling / Disabling](#enabling--disabling-1)
51+
- [Specify Input or Inputpath or InputTransformer](#specify-input-or-inputpath-or-inputtransformer)
52+
- [Specifying a Description](#specifying-a-description)
53+
- [Specifying a Name](#specifying-a-name)
54+
- [Specifying a RoleArn](#specifying-a-rolearn)
55+
- [Specifying a custom CloudWatch EventBus](#specifying-a-custom-cloudwatch-eventbus)
56+
- [Specifying a custom EventBridge EventBus](#specifying-a-custom-eventbridge-eventbus)
57+
- [Specifying a DeadLetterQueue](#specifying-a-deadletterqueue)
58+
- [Tags](#tags)
59+
- [Commands](#commands)
60+
- [deploy](#deploy)
61+
- [invoke](#invoke)
62+
- [IAM Role](#iam-role)
63+
- [Tips](#tips)
64+
- [How to specify the stateMachine ARN to environment variables](#how-to-specify-the-statemachine-arn-to-environment-variables)
65+
- [How to split up state machines into files](#how-to-split-up-state-machines-into-files)
66+
- [Sample statemachines setting in serverless.yml](#sample-statemachines-setting-in-serverlessyml)
67+
- [Wait State](#wait-state)
68+
- [Retry Failure](#retry-failure)
69+
- [Parallel](#parallel)
70+
- [Catch Failure](#catch-failure)
71+
- [Choice](#choice)
72+
- [Map](#map)
7373

7474
## Install
7575

@@ -425,8 +425,8 @@ stepFunctions:
425425
- lambda: LAMBDA_FUNCTION_ARN
426426
- kinesis: KINESIS_STREAM_ARN
427427
- kinesis:
428-
arn: KINESIS_STREAM_ARN
429-
partitionKeyPath: $.id # used to choose the parition key from payload
428+
arn: KINESIS_STREAM_ARN
429+
partitionKeyPath: $.id # used to choose the parition key from payload
430430
- firehose: FIREHOSE_STREAM_ARN
431431
- stepFunctions: STATE_MACHINE_ARN
432432
FAILED:
@@ -790,7 +790,7 @@ stepFunctions:
790790
createUser:
791791
...
792792
events:
793-
- http:
793+
- http:
794794
path: /users
795795
...
796796
authorizer:
@@ -873,6 +873,43 @@ stepFunctions:
873873
definition:
874874
```
875875

876+
If you want to add multiple custom templates for different status codes, headers and content types, you can do so by including them in the `responses` object like so:
877+
878+
```yml
879+
880+
stepFunctions:
881+
stateMachines:
882+
hello:
883+
events:
884+
- http:
885+
path: posts/create
886+
method: POST
887+
responses:
888+
200:
889+
statusCode: 200
890+
responseParameters:
891+
method.response.header.Content-Type: "'application/json'"
892+
method.response.header.X-Application-Id: "'my-app'"
893+
responseTemplates:
894+
application/json: |
895+
{
896+
"status": 200,
897+
"info": "OK"
898+
}
899+
400:
900+
statusCode: 400
901+
responseParameters:
902+
method.response.header.Content-Type: "'application/json'"
903+
method.response.header.X-Application-Id: "'my-app'"
904+
responseTemplates:
905+
application/json: |
906+
{
907+
"status": 400,
908+
"info": "Bad Request"
909+
}
910+
definition:
911+
```
912+
876913
#### Send request to an API
877914

878915
You can input an value as json in request body, the value is passed as the input value of your statemachine
@@ -982,7 +1019,7 @@ provider:
9821019
name: PostCreateModel
9831020
schema: ${file(api_schema/post_add_schema.json)}
9841021
description: "A Model validation for adding posts"
985-
1022+
9861023
stepFunctions:
9871024
stateMachines:
9881025
create:
@@ -1089,10 +1126,10 @@ stepFunctions:
10891126
stateMachines:
10901127
stateMachineScheduled:
10911128
events:
1092-
- schedule:
1129+
- schedule:
10931130
rate: cron(30 12 ? * 1-5 *)
10941131
inputTransformer:
1095-
inputPathsMap:
1132+
inputPathsMap:
10961133
time: '$.time'
10971134
stage: '$.stageVariables'
10981135
inputTemplate: '{"time": <time>, "stage" : <stage> }'
@@ -1424,8 +1461,8 @@ Then
14241461
# to get the Arn of the 1st EventBridge rule
14251462
!GetAtt Hellostepfunc1EventsRuleCloudWatchEvent1.Arn
14261463

1427-
# to get the Arn of the 2nd EventBridge rule
1428-
!GetAtt Hellostepfunc1EventsRuleCloudWatchEvent2.Arn
1464+
# to get the Arn of the 2nd EventBridge rule
1465+
!GetAtt Hellostepfunc1EventsRuleCloudWatchEvent2.Arn
14291466
```
14301467

14311468
## Tags
@@ -1505,12 +1542,12 @@ resources:
15051542
Path: /path_of_state_machine_roles/
15061543
AssumeRolePolicyDocument:
15071544
Statement:
1508-
- Effect: Allow
1509-
Principal:
1510-
Service:
1511-
- states.amazonaws.com
1512-
Action:
1513-
- sts:AssumeRole
1545+
- Effect: Allow
1546+
Principal:
1547+
Service:
1548+
- states.amazonaws.com
1549+
Action:
1550+
- sts:AssumeRole
15141551
Policies:
15151552
- PolicyName: statePolicy
15161553
PolicyDocument:
@@ -1740,21 +1777,21 @@ stepFunctions:
17401777
Type: Parallel
17411778
Next: Final State
17421779
Branches:
1743-
- StartAt: Wait 20s
1744-
States:
1745-
Wait 20s:
1746-
Type: Wait
1747-
Seconds: 20
1748-
End: true
1749-
- StartAt: Pass
1750-
States:
1751-
Pass:
1752-
Type: Pass
1753-
Next: Wait 10s
1754-
Wait 10s:
1755-
Type: Wait
1756-
Seconds: 10
1757-
End: true
1780+
- StartAt: Wait 20s
1781+
States:
1782+
Wait 20s:
1783+
Type: Wait
1784+
Seconds: 20
1785+
End: true
1786+
- StartAt: Pass
1787+
States:
1788+
Pass:
1789+
Type: Pass
1790+
Next: Wait 10s
1791+
Wait 10s:
1792+
Type: Wait
1793+
Seconds: 10
1794+
End: true
17581795
Final State:
17591796
Type: Pass
17601797
End: true
@@ -1782,12 +1819,12 @@ stepFunctions:
17821819
Resource:
17831820
Fn::GetAtt: [hello, Arn]
17841821
Catch:
1785-
- ErrorEquals: ["HandledError"]
1786-
Next: CustomErrorFallback
1787-
- ErrorEquals: ["States.TaskFailed"]
1788-
Next: ReservedTypeFallback
1789-
- ErrorEquals: ["States.ALL"]
1790-
Next: CatchAllFallback
1822+
- ErrorEquals: ["HandledError"]
1823+
Next: CustomErrorFallback
1824+
- ErrorEquals: ["States.TaskFailed"]
1825+
Next: ReservedTypeFallback
1826+
- ErrorEquals: ["States.ALL"]
1827+
Next: CatchAllFallback
17911828
End: true
17921829
CustomErrorFallback:
17931830
Type: Pass
@@ -1834,12 +1871,12 @@ stepFunctions:
18341871
ChoiceState:
18351872
Type: Choice
18361873
Choices:
1837-
- Variable: "$.foo"
1838-
NumericEquals: 1
1839-
Next: FirstMatchState
1840-
- Variable: "$.foo"
1841-
NumericEquals: 2
1842-
Next: SecondMatchState
1874+
- Variable: "$.foo"
1875+
NumericEquals: 1
1876+
Next: FirstMatchState
1877+
- Variable: "$.foo"
1878+
NumericEquals: 2
1879+
Next: SecondMatchState
18431880
Default: DefaultState
18441881
FirstMatchState:
18451882
Type: Task

‎lib/deploy/events/apiGateway/methods.js

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ const LAMBDA_PROXY_FORM_URL_ENCODED_REQUEST_TEMPLATE = `
103103
#end
104104
${LAMBDA_PROXY_REQUEST_TEMPLATE}`;
105105

106+
function generateResponseParameters(responseHeaders) {
107+
return _.mapKeys(
108+
responseHeaders,
109+
(value, key) => `method.response.header.${key}`,
110+
);
111+
}
112+
106113
module.exports = {
107114

108115
compileMethods() {
@@ -186,29 +193,49 @@ module.exports = {
186193
),
187194
};
188195

189-
const responseParams = _.mapKeys(
190-
_.get(http, 'response.headers', {}),
191-
(value, key) => `method.response.header.${key}`,
192-
);
193-
const responseTemplates = _.get(http, 'response.template', {});
194-
195-
const integrationResponse = {
196-
IntegrationResponses: [
197-
{
198-
StatusCode: 200,
199-
SelectionPattern: 200,
200-
ResponseParameters: responseParams,
201-
ResponseTemplates: responseTemplates,
196+
let responses;
197+
198+
if (_.has(http, 'responses')) {
199+
responses = _.get(http, 'responses', {});
200+
} else {
201+
const response = _.get(http, 'response');
202+
const responseHeaders = _.get(response, 'headers', {});
203+
const responseTemplates = _.get(response, 'template', {});
204+
const responseParameters = generateResponseParameters(responseHeaders);
205+
responses = {
206+
200: {
207+
statusCode: 200,
208+
selectionPattern: 200,
209+
responseParameters,
210+
responseTemplates,
202211
},
203-
{
204-
StatusCode: 400,
205-
SelectionPattern: 400,
206-
ResponseParameters: {},
207-
ResponseTemplates: {},
212+
400: {
213+
statusCode: 400,
214+
selectionPattern: 400,
215+
responseParameters: {},
216+
responseTemplates: {},
208217
},
209-
],
218+
};
219+
}
220+
221+
const integrationResponse = {
222+
IntegrationResponses: [],
210223
};
211224

225+
_.forOwn(responses, (value, key) => {
226+
const responseParameters = _.get(value, 'responseParameters', {});
227+
const responseTemplates = _.get(value, 'responseTemplates', {});
228+
const statusCode = _.get(value, 'statusCode', _.toInteger(key));
229+
const selectionPattern = _.get(value, 'selectionPattern', _.toInteger(key));
230+
231+
integrationResponse.IntegrationResponses.push({
232+
StatusCode: statusCode,
233+
SelectionPattern: selectionPattern,
234+
ResponseParameters: responseParameters,
235+
ResponseTemplates: responseTemplates,
236+
});
237+
});
238+
212239
if (http && http.cors) {
213240
let origin = http.cors.origin;
214241
if (http.cors.origins && http.cors.origins.length) {

‎lib/deploy/events/apiGateway/methods.test.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,155 @@ describe('#methods()', () => {
191191
expect(intResponseParams['method.response.header.X-Application-Id'])
192192
.to.be.equal('id');
193193
});
194+
195+
it('should return custom headers and template when given',
196+
() => {
197+
const httpWithResponseHeaders = {
198+
path: 'foo/bar1',
199+
method: 'post',
200+
response: {
201+
headers: {
202+
'Content-Type': 'text',
203+
'X-Application-Id': 'id',
204+
},
205+
template: {
206+
'application/json': 'custom template',
207+
},
208+
},
209+
};
210+
const resource = serverlessStepFunctions
211+
.getMethodIntegration('stateMachine', undefined, httpWithResponseHeaders);
212+
213+
const integrationResponses = resource
214+
.Properties.Integration.IntegrationResponses
215+
.find(x => x.StatusCode === 200);
216+
expect(integrationResponses.ResponseParameters['method.response.header.Content-Type'])
217+
.to.be.equal('text');
218+
expect(integrationResponses.ResponseParameters['method.response.header.X-Application-Id'])
219+
.to.be.equal('id');
220+
expect(integrationResponses.ResponseTemplates['application/json'])
221+
.to.be.equal('custom template');
222+
});
223+
224+
it('should return a custom template for application/json when responses are given',
225+
() => {
226+
const httpWithResponseTemplate = {
227+
path: 'foo/bar1',
228+
method: 'post',
229+
responses: {
230+
200: {
231+
statusCode: 200,
232+
responseTemplates: {
233+
'application/json': 'custom template',
234+
},
235+
},
236+
},
237+
};
238+
const resource = serverlessStepFunctions
239+
.getMethodIntegration('stateMachine', undefined, httpWithResponseTemplate);
240+
const responseTemplates = resource
241+
.Properties.Integration.IntegrationResponses
242+
.find(x => x.StatusCode === 200)
243+
.ResponseTemplates;
244+
expect(responseTemplates['application/json'])
245+
.to.be.equal('custom template');
246+
});
247+
248+
it('should return multiple custom templates for application/json when responses are given',
249+
() => {
250+
const httpWithResponseTemplate = {
251+
path: 'foo/bar1',
252+
method: 'post',
253+
responses: {
254+
200: {
255+
statusCode: 200,
256+
responseTemplates: {
257+
'application/json': 'custom 200 template',
258+
},
259+
},
260+
400: {
261+
statusCode: 400,
262+
responseTemplates: {
263+
'application/json': 'custom 400 template',
264+
},
265+
},
266+
},
267+
};
268+
const resource = serverlessStepFunctions
269+
.getMethodIntegration('stateMachine', undefined, httpWithResponseTemplate);
270+
const response200Templates = resource
271+
.Properties.Integration.IntegrationResponses
272+
.find(x => x.StatusCode === 200)
273+
.ResponseTemplates;
274+
expect(response200Templates['application/json'])
275+
.to.be.equal('custom 200 template');
276+
const response400Templates = resource
277+
.Properties.Integration.IntegrationResponses
278+
.find(x => x.StatusCode === 400)
279+
.ResponseTemplates;
280+
expect(response400Templates['application/json'])
281+
.to.be.equal('custom 400 template');
282+
});
283+
284+
it('should return custom headers when when responses are given',
285+
() => {
286+
const httpWithResponseHeaders = {
287+
path: 'foo/bar1',
288+
method: 'post',
289+
responses: {
290+
200: {
291+
responseParameters: {
292+
'method.response.header.Content-Type': 'text',
293+
'method.response.header.X-Application-Id': 'id',
294+
},
295+
},
296+
},
297+
};
298+
const resource = serverlessStepFunctions
299+
.getMethodIntegration('stateMachine', undefined, httpWithResponseHeaders);
300+
301+
const intResponseParams = resource
302+
.Properties.Integration.IntegrationResponses
303+
.find(x => x.StatusCode === 200)
304+
.ResponseParameters;
305+
expect(intResponseParams['method.response.header.Content-Type'])
306+
.to.be.equal('text');
307+
expect(intResponseParams['method.response.header.X-Application-Id'])
308+
.to.be.equal('id');
309+
});
194310
});
195311

312+
it('should return custom headers and template when responses are given',
313+
() => {
314+
const httpWithResponseHeaders = {
315+
path: 'foo/bar1',
316+
method: 'post',
317+
responses: {
318+
200: {
319+
responseParameters: {
320+
'method.response.header.Content-Type': 'application/json',
321+
'method.response.header.X-Application-Id': 'id',
322+
},
323+
responseTemplates: {
324+
'application/json': 'custom template',
325+
},
326+
},
327+
},
328+
};
329+
const resource = serverlessStepFunctions
330+
.getMethodIntegration('stateMachine', undefined, httpWithResponseHeaders);
331+
332+
const integrationResponses = resource
333+
.Properties.Integration.IntegrationResponses
334+
.find(x => x.StatusCode === 200);
335+
expect(integrationResponses.ResponseParameters['method.response.header.Content-Type'])
336+
.to.be.equal('application/json');
337+
expect(integrationResponses.ResponseParameters['method.response.header.X-Application-Id'])
338+
.to.be.equal('id');
339+
expect(integrationResponses.ResponseTemplates['application/json'])
340+
.to.be.equal('custom template');
341+
});
342+
196343
describe('#getIntegrationRequestTemplates()', () => {
197344
it('should set stateMachinelogical ID in default templates when customName is not set', () => {
198345
const requestTemplates = serverlessStepFunctions

0 commit comments

Comments
 (0)
Please sign in to comment.