Configuring Serverless Backend CRUD Operations with AWS API Gateway and DynamoDB: A No-Lambda Approach

2022-12-27

#cloud#backend
Configuring Serverless Backend CRUD Operations with AWS API Gateway and DynamoDB: A No-Lambda Approach

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.