This is a series of articles about setting up a complex Serverless backend infrastructure with AWS SAM and CloudFormation.
Here is the index of all the articles in case you want to jump to any of them:
1. Setup of AppSync and API Gateway with Multiple AWS Cognito User Pools
2. Configuring S3 Buckets with Permissions and Access Roles in AWS Cognito AuthRole
3. Intro to DynamoDB Resolvers for AppSync Implementation
4. Intro to Lambda Resolvers for AppSync Implementation
5. Configuring an AWS VPC to Include Lambda Resolvers with a Fixed IP
- Intro to Pipeline Resolvers for AppSync Implementation
7. Handling Lambda Resolver Timeouts with SNS Messages
Your Lambda resolver is doing three things: checking permissions, logging the request, and executing the mutation. One function, three responsibilities. When the permission check breaks, you're debugging inside the same function that handles data mutations. When the logging changes, you're touching code that also writes to DynamoDB.
Pipeline resolvers fix this. They chain multiple functions sequentially — each one does exactly one thing, and the output of one feeds into the next.
A typical pipeline:
- Verify the user's role
- Log the request to an audit tool
- Execute the actual operation
Three functions. Three concerns. Each testable and replaceable independently.
The CloudFormation Setup
Resources:
# ... [Previous resources from the template]
# UpdateUserFunction
UpdateUserFunction:
Type: "AWS::AppSync::FunctionConfiguration"
Properties:
ApiId: !GetAtt AppSyncAPI.ApiId
Name: "UpdateUserFunction"
DataSourceName: !GetAtt PipelineFunctionDataSource.Name
FunctionVersion: "2018-05-29"
RequestMappingTemplate: |
{
"version": "2018-05-29",
"operation": "Invoke",
"payload": {
"operation": "updateUser",
"args": $util.toJson($context.args)
}
}
ResponseMappingTemplate: "$util.toJson($context.result)"
# Pipeline Resolver for updateUser
UpdateUserResolver:
Type: "AWS::AppSync::Resolver"
Properties:
ApiId: !GetAtt AppSyncAPI.ApiId
TypeName: "Mutation"
FieldName: "updateUser"
Kind: "PIPELINE"
PipelineConfig:
Functions:
- !GetAtt VerifyUserRoleFunction.FunctionId
- !GetAtt LogActionFunction.FunctionId
- !GetAtt UpdateUserFunction.FunctionId
RequestMappingTemplate: "{}" # No-op since the logic is in the functions
ResponseMappingTemplate: "$util.toJson($context.prev.result)"
# ... [Other resources like IAM roles, AppSync API, etc.]
The resolver's Kind is PIPELINE. The PipelineConfig lists the functions in execution order. The request mapping is a no-op because each function handles its own logic. The response returns whatever the last function produced.
The real power: that VerifyUserRoleFunction is reusable across every mutation that needs authorization. Write it once, add it to any pipeline. Same for logging, rate limiting, or input validation.
Single-function resolvers are fine when the operation is simple. The moment you're mixing concerns in one function, break it into a pipeline. Your future debugging self will thank you.
Code examples are simplified to illustrate the approach. Some adjustments may be needed for production.
Next UP: Part 7. Handling Lambda Resolver Timeouts with SNS Messages
