You need a CRUD API. You reach for Lambda because that's what every tutorial says. But if your operations are straightforward reads and writes — no business logic, no transformations — Lambda is unnecessary overhead. API Gateway can talk to DynamoDB directly.
No compute layer. No cold starts. No Lambda code to maintain. Just a mapping template that translates HTTP requests into DynamoDB operations and back.
The Simple Version: No Auth
A GET endpoint that reads from a Users table. API Gateway sends a GetItem request directly to DynamoDB, maps the response back to JSON, and returns it. The entire backend is infrastructure.
Resources:
MyDynamoDBTable:
Type: 'AWS::DynamoDB::Table'
Properties:
TableName: Users
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
MyApi:
Type: 'AWS::ApiGateway::RestApi'
Properties:
Name: UsersApi
UsersResource:
Type: 'AWS::ApiGateway::Resource'
Properties:
RestApiId: !Ref MyApi
ParentId: !GetAtt
- MyApi
- RootResourceId
PathPart: users
GetMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
RestApiId: !Ref MyApi
ResourceId: !Ref UsersResource
HttpMethod: GET
AuthorizationType: NONE
Integration:
Type: AWS
IntegrationHttpMethod: POST
Uri:
Fn::Sub:
- 'arn:aws:apigateway:${AWS::Region}:dynamodb:action/GetItem'
- { AWS::Region: !Ref "AWS::Region" }
PassthroughBehavior: WHEN_NO_TEMPLATES
RequestTemplates:
application/json: |
{
"TableName": "Users",
"Key": {
"userId": {
"S": "$input.params('userId')"
}
}
}
IntegrationResponses:
- StatusCode: 200
ResponseTemplates:
application/json: |
#set($inputRoot = $input.path('$'))
{
"userId": "$inputRoot.Item.userId.S",
"name": "$inputRoot.Item.name.S",
"email": "$inputRoot.Item.email.S"
}
MethodResponses:
- StatusCode: 200
Adding JWT Authentication
Drop a JWT authorizer in front of it. Same DynamoDB integration, now protected.
Resources:
# ... (same as above)
JwtAuthorizer:
Type: 'AWS::ApiGateway::Authorizer'
Properties:
Name: JwtAuthorizer
RestApiId: !Ref MyApi
Type: TOKEN
IdentitySource: method.request.header.Authorization
JwtConfiguration:
issuer: YOUR_ISSUER_URL
audience:
- YOUR_AUDIENCE
GetMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
# ... (same as above)
AuthorizationType: CUSTOM
AuthorizerId: !Ref JwtAuthorizer
Adding Cognito Authentication
Same pattern, different authorizer. Point it at a Cognito User Pool instead.
Resources:
# ... (same as above)
CognitoAuthorizer:
Type: 'AWS::ApiGateway::Authorizer'
Properties:
Name: CognitoAuthorizer
RestApiId: !Ref MyApi
Type: COGNITO_USER_POOLS
IdentitySource: method.request.header.Authorization
ProviderARNs:
- YOUR_USER_POOL_ARN
GetMethod:
Type: 'AWS::ApiGateway::Method'
Properties:
# ... (same as above)
AuthorizationType: COGNITO_USER_POOLS
AuthorizerId: !Ref CognitoAuthorizer
Replace placeholders with your actual values. These templates cover the GET method for simplicity — add similar configurations for POST, PUT, and DELETE.
The Tradeoff
This approach breaks down the moment you need business logic — validation, transformations, calls to other services. But for simple CRUD against a single table, it's fewer moving parts, fewer things to break, and fewer things to pay for.
If your Lambda function is just passing data between API Gateway and DynamoDB without touching it, you don't need the Lambda.
