What is the proper way to return data with HTTP API + Lambda + DynamoDB as a json response? (added a Python example)

1

I used to use the REST api, but since v2 i find myself using it more. Is there a proper way to return data "neatly" other than manipulating the database response before returning? I used to use the model feature with REST (v1). I was wondering what's the recommended way to do the same here. tnx.

Here's an example of what i'm trying to do. I'm selecting specific columns, while avoiding the error:

An error occurred (ValidationException) when calling the UpdateItem operation: Invalid UpdateExpression: Attribute name is a reserved keyword "owner"

and since integers/floats return as a "Decimal":

Object of type Decimal is not JSON serializable

i added the class to set them properly as integers/floats.

I'd be happy to get some heads up unrelated to my question too. Tnx.

import json
import boto3
from decimal import Decimal

class DecimalEncoder(json.JSONEncoder):
  def default(self, obj):
    if isinstance(obj, Decimal):
      return str(obj)
    return json.JSONEncoder.default(self, obj)


def lambda_handler(event, context):
    try:
        dynamodb = boto3.resource('dynamodb')
        table = dynamodb.Table('SomeTable')
        response_body = ''
        status_code = 0
        response = table.scan(
            ProjectionExpression="#col1, #col2, #col3, #col4, #col5",
            ExpressionAttributeNames={
                "#col1": "col1",
                "#col2": "col2",
                "#col3": "col3",
                "#col4": "col4",
                "#col5": "col5"
            }
        )
        items = response["Items"]
        mapped_items = list(map(lambda item: {
            'col1': item['col1'],
            'col2': item['col2'],
            'col3': item['col3'],
            'col4': item['col4'],
            'col5': item['col5'],
        }, items))
        response_body = json.dumps(mapped_items, cls=DecimalEncoder)
        status_code = 200

    except Exception as e:
        response_body = json.dumps(
            {'error': 'Unable to get metadata from SomeTable: ' + str(e)})
        status_code = 403

    json_response = {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json"
        },
        "body": response_body
    }

    return json_response

This just looks too much for a simple "GET" request of some columns in a table

1 Answer
1
Accepted Answer

Your question and objective have multiple pieces:

import json
import uuid
import os
import boto3
from datetime import datetime
from decimal import Decimal

class DecimalEncoder(json.JSONEncoder):
  def default(self, obj):
    if isinstance(obj, Decimal):
      return str(obj)
    return json.JSONEncoder.default(self, obj)
# Prepare DynamoDB client
USERS_TABLE = os.getenv('USERS_TABLE', None)
dynamodb = boto3.resource('dynamodb')
ddbTable = dynamodb.Table(USERS_TABLE)

def lambda_handler(event, context):
    route_key = f"{event['httpMethod']} {event['resource']}"

    # Set default response, override with data from DynamoDB if any
    response_body = {'Message': 'Unsupported route'}
    status_code = 400
    headers = {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
        }

    try:
        # Get a list of all Users
        if route_key == 'GET /users':
            ddb_response = ddbTable.scan(Select='ALL_ATTRIBUTES')
            # return list of items instead of full DynamoDB response
            response_body = ddb_response['Items']
            status_code = 200

        # CRUD operations for a single User
       
        # Read a user by ID
        if route_key == 'GET /users/{userid}':
            # get data from the database
            ddb_response = ddbTable.get_item(
                Key={'userid': event['pathParameters']['userid']}
            )
            # return single item instead of full DynamoDB response
            if 'Item' in ddb_response:
                response_body = ddb_response['Item']
            else:
                response_body = {}
            status_code = 200
        
    except Exception as err:
        status_code = 400
        response_body = {'Error:': str(err)}
        print(str(err))
    return {
        'statusCode': status_code,
        'body': json.dumps(response_body, cls=DecimalEncoder),
        'headers': headers
    }

Your mapped_items object is just remapping the same values as exist in the items response, so if you don't need to rename those values in the json, you don't need that step. For example, this is the body of your response with and without the mapped_items:

With mapped_items

[{\"col1\": \"1\", \"col2\": \"2\", \"col3\": \"3\", \"col4\": \"4\", \"col5\": \"5\", \"owner\": \"ddd\"}]

Without mapped_items (just returning response["Items"])

[{\"col3\": \"3\", \"col4\": \"4\", \"col5\": \"5\", \"owner\": \"ddd\", \"col1\": \"1\", \"col2\": \"2\"}]

Also, it is better practice to establish the dynamoDB connection outside of the request handler (this is demonstrated in the above code). That way, if a warm invocation occurs, that invocation will use the existing dynamodDB connection and complete quicker

References: https://catalog.workshops.aws/serverless-patterns/en-US

profile pictureAWS
answered 8 months ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions