DEV Community

FeliDrummond
FeliDrummond

Posted on

From Chaos to REST: How I Evolved My First AWS Lambda API

In my last post, I talked about how I started using AWS Lambda and Amazon API Gateway to build my first API Building My First AWS Lambda API. But to be honest, what I had in the beginning wasn’t exactly a well-structured API. It was functional, but far from ideal. In this post, I want to show the real evolution of my code, going through three stages: a “task executor” (RPC/tunneling style), an API with standardized responses and error handling, and finally a proper REST API.

At first, everything revolved around a single endpoint using the POST method. The logic depended on a field called "tarefa" (task) inside the request body. In other words, instead of using HTTP itself to express the intention of the operation, I created a kind of manual command system. Something like "tarefa": "delete_log" or "tarefa": "get_log". Although this works, this model follows a style closer to RPC (Remote Procedure Call), also known as tunneling. In practice, the API becomes a command executor, where every decision is handled inside a large conditional block with multiple if/elif statements.

This approach introduced several important problems. The first one was completely ignoring HTTP semantics. Methods like GET, POST, PUT, and DELETE already have well-defined meanings, and by using only POST for everything, I lost clarity, standardization, and compatibility with the web ecosystem. Another issue was high coupling: everything depended on the "tarefa" field, concentrating multiple responsibilities into a single function, which makes the code harder to maintain and scale. Additionally, the responses were inconsistent — sometimes a plain string, sometimes JSON — which makes frontend consumption much harder. There was also no proper handling of HTTP status codes: even when errors occurred, the API would still return status 200. Finally, there was no exception handling, so any unexpected error could break the function entirely.

In the second stage, I started organizing the code better. I introduced a standardized response format with fields like "success", "message", and "data", ensuring that every response had a predictable structure. I also began properly handling HTTP status codes, using values like 200, 400, 404, and 500 depending on the situation. Another important improvement was adding a try/except block, ensuring the API would always return a response, even in case of internal errors. I also defined default values at the beginning of the function — such as status, success, message, and data — and let each block modify only what was necessary. This brought a significant improvement in organization and clarity. However, the structure was still based on a single endpoint and a single method, with all logic depending on the "tarefa" field.

That’s when the third stage came in: adopting the REST model. This required an important shift in mindset. I stopped thinking in terms of actions and started thinking in terms of resources. Instead of asking “which task should be executed?”, I started asking “which resource am I interacting with and what operation am I performing on it?”. With this change, the intention of the request became defined by the combination of HTTP method and route, even before looking at the request body. For example: GET /logs to list logs, GET /logs/{id} to retrieve a specific log, POST /logs to create, PUT /logs/{id} to update, and DELETE /logs/{id} to delete. In this model, there is no longer a need for a large if tarefa == ... block, because each route already represents a specific responsibility.

This change brought clear benefits. The organization improved significantly, the code became more readable, and maintenance became simpler. The API started to follow HTTP standards, making integrations with tools and other systems easier. There was also a significant reduction in coupling, since each route now handles a specific responsibility instead of centralizing everything in one place. In terms of scalability and clarity, the difference between a single-endpoint model and REST is huge.

The main lesson from this journey is that you don’t need to start perfectly. I began with something simple that worked, then organized it, standardized it, and only then evolved to a more robust model. When I started, I thought I was building an API, but in practice, I was building a disguised command executor — and that’s okay. This kind of evolution is part of the learning process.

As next steps, I plan to keep improving this API by adding authentication and setting up an automated deployment pipeline using GitHub Actions, so I no longer need to manually update the Lambda function through the AWS console. If you’re just starting out, you’ll probably make these same “mistakes” — and that’s part of the process. What matters is not getting everything right from the beginning, but understanding what you’re doing and evolving consistently.

```python title="lambda_function.py
Código 1:
import json
import boto3
import uuid
from boto3.dynamodb.conditions import Attr

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('LogsAutomacao')

def lambda_handler(event, context):
body = json.loads(event.get("body", "{}"))

tarefa = body.get("tarefa", "nenhuma")
mensagem = body.get("mensagem", "")


if tarefa == "log":
    table.put_item(Item={
        'id': str(uuid.uuid4()),
        'mensagem': mensagem
    })
    resposta =  f"Log salvo com sucesso: {mensagem}"

elif tarefa == "atualizar_log":
    id_log = body.get("id")
    nova_mensagem = body.get("mensagem")


    if not id_log or not nova_mensagem:
        resposta = "ID ou nova mensagem não fornecidos"
    else:
        response = table.update_item(Key = {"id":id_log},            
        UpdateExpression="SET mensagem = :m",
        ExpressionAttributeValues={
            ':m': nova_mensagem
        })
        resposta = f"Log atualizado com sucesso: {id_log}"

elif tarefa == "filtrar_logs":
    termo = body.get("mensagem", "")
    if not termo:
        resposta = "Termo de busca não fornecido"
    else:
        response = table.scan(FilterExpression=Attr('mensagem').contains(termo))
        itens = response.get('Items', [])
        resposta = json.dumps(itens)


elif tarefa == "buscar_log":
    id_log = body.get("id")  
    if not id_log:
        resposta = "ID não fornecido"
    else:
        response = table.get_item(Key = {"id":id_log})
        item = response.get("Item")


    if item:
        resposta = json.dumps(item)
    else:
        resposta = "Log não encontrado"


elif  tarefa == "deletar_log":
    id_log = body.get("id")


    if not id_log:
        resposta = "ID não fornecido"
    else:
        table.delete_item(
            Key={
                'id': id_log
            }
        )
        resposta = f"Log deletado com sucesso: {id_log}"




elif tarefa == "listar_logs":


    response = table.scan()
    itens = response.get('Items', [])


    resposta = json.dumps(itens)



elif tarefa == "processar":
    numeros = body.get("numeros", [])
    if not numeros:
        resposta = "Nenhum número enviado"
    else:
        soma = sum(numeros)
        media = soma / len(numeros)
        quantidade = len(numeros)

        resposta = f"Soma:{soma} | Média:{media} | Quantidade:{quantidade}"



else:
    resposta = "Tarefa não reconhecida"


return {
    "statusCode": 200,
    "body": resposta
}
Enter fullscreen mode Exit fullscreen mode

Código 2:
import json
import boto3
import uuid
from boto3.dynamodb.conditions import Attr

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('LogsAutomacao')

def lambda_handler(event, context):
body = json.loads(event.get("body", "{}"))
tarefa = body.get("tarefa", "nenhuma")

    # Configuração padrão da resposta
status = 200
sucesso = True
mensagem = ""
dados = None
try:
    if tarefa == "log":
        msg_log = body.get("mensagem", "")
        id_log = str(uuid.uuid4())
        table.put_item(Item={
            'id': id_log,
            'mensagem': msg_log
        })
        mensagem = "Log salvo com sucesso"
        dados = {
        "id": id_log,
        "mensagem": msg_log
        }



    elif tarefa == "atualizar_log":
        id_log = body.get("id")
        nova_mensagem = body.get("mensagem")


        if not id_log or not nova_mensagem:
            status, sucesso, mensagem = 400, False, "ID ou nova mensagem não fornecidos"


        else:
            response = table.update_item(Key = {"id":id_log},            
            UpdateExpression="SET mensagem = :m",
            ExpressionAttributeValues={
                ':m': nova_mensagem
            })
            mensagem = f"Log {id_log} atualizado"

    elif tarefa == "filtrar_logs":
        termo = body.get("mensagem", "")
        if not termo:
            status, sucesso, mensagem = 400, False, "Termo de busca não fornecido"
        else:
            response = table.scan(FilterExpression=Attr('mensagem').contains(termo))
            dados = response.get('Items', [])
            if not dados:
                status, sucesso, mensagem = 404, False, "Termo de busca não encontrado"
            else:
                 mensagem = f"Busca concluída. Encontrados: {len(dados)}"



    elif tarefa == "buscar_log":
        id_log = body.get("id")  
        if not id_log:
            status, sucesso, mensagem = 400, False, "ID não fornecido"
        else:
            response = table.get_item(Key = {"id":id_log})
            dados = response.get("Item")
            if not dados:
                status, sucesso, mensagem = 404, False, "Log não encontrado"
            else:
                mensagem = "Log recuperado com sucesso"




    elif  tarefa == "deletar_log":
        id_log = body.get("id")


        if not id_log:
            status, sucesso, mensagem = 400, False, "ID não fornecido"
        else:
            table.delete_item(
                Key={
                    'id': id_log
                }
            )
            mensagem = f"Log deletado com sucesso: ID = {id_log}"            





    elif tarefa == "listar_logs":


        response = table.scan()
        dados = response.get('Items', [])
        mensagem = f"Total de logs: {len(dados)}"






    elif tarefa == "processar":
        numeros = body.get("numeros", [])
        if not numeros or not isinstance(numeros, list):
            status, sucesso, mensagem = 400, False, "Lista de números inválida ou vazia"
        else:
            soma = sum(numeros)
            dados = {
                "soma": soma,
                "media": soma / len(numeros),
                "quantidade": len(numeros)
            }
            mensagem = "Cálculos realizados"



    else:
        status, sucesso, mensagem = 400, False, "Tarefa não reconhecida"
except Exception as e:
    # Captura erros inesperados (ex: erro de permissão no DynamoDB)
    status, sucesso, mensagem = 500, False, f"Erro interno: {str(e)}"


return {
"statusCode": status,
"headers": {
    "Content-Type": "application/json",
    "Access-Control-Allow-Origin": "*" # Útil se for usar em um site (CORS)
},
"body": json.dumps({
    "sucesso": sucesso,
    "mensagem": mensagem,
    "dados": dados
})
}
Enter fullscreen mode Exit fullscreen mode

Código 3:
import json
import boto3
import uuid
from boto3.dynamodb.conditions import Attr

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('LogsAutomacao')

def resposta(status, sucesso, mensagem, dados=None):
return {
"statusCode": status,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
"body": json.dumps({
"sucesso": sucesso,
"mensagem": mensagem,
"dados": dados
})
}

def lambda_handler(event, context):

metodo = event["requestContext"]["http"]["method"]
path = event["rawPath"].replace("/dev", "")
path_params = event.get("pathParameters") or {}
query_params = event.get("queryStringParameters") or {}

body = json.loads(event.get("body", "{}"))

try:

    # 🔹 POST /logs (CRIAR)
    if metodo == "POST" and path == "/logs":
        msg = body.get("mensagem", "")
        id_log = str(uuid.uuid4())

        table.put_item(Item={
            "id": id_log,
            "mensagem": msg
        })

        return resposta(201, True, "Log criado", {
            "id": id_log,
            "mensagem": msg
        })

    # 🔹 GET /logs (LISTAR ou FILTRAR)
    elif metodo == "GET" and path == "/logs":

        termo = query_params.get("mensagem")

        if termo:
            response = table.scan(
                FilterExpression=Attr('mensagem').contains(termo)
            )
        else:
            response = table.scan()

        return resposta(200, True, "Logs retornados", response.get("Items", []))

    # 🔹 GET /logs/{id}
    elif metodo == "GET" and "id" in path_params:
        id_log = path_params["id"]

        response = table.get_item(Key={"id": id_log})
        item = response.get("Item")

        if not item:
            return resposta(404, False, "Log não encontrado")

        return resposta(200, True, "Log encontrado", item)

    # 🔹 PUT /logs/{id}
    elif metodo == "PUT" and "id" in path_params:
        id_log = path_params["id"]
        nova_mensagem = body.get("mensagem")

        if not nova_mensagem:
            return resposta(400, False, "Mensagem obrigatória")

        table.update_item(
            Key={"id": id_log},
            UpdateExpression="SET mensagem = :m",
            ExpressionAttributeValues={":m": nova_mensagem}
        )

        return resposta(200, True, "Log atualizado")

    # 🔹 DELETE /logs/{id}
    elif metodo == "DELETE" and "id" in path_params:
        id_log = path_params["id"]

        table.delete_item(Key={"id": id_log})

        return resposta(200, True, "Log deletado")

    # 🔹 POST /processar
    elif metodo == "POST" and path == "/processar":
        numeros = body.get("numeros", [])

        if not numeros:
            return resposta(400, False, "Lista inválida")

        soma = sum(numeros)

        return resposta(200, True, "Processado", {
            "soma": soma,
            "media": soma / len(numeros),
            "quantidade": len(numeros)
        })

    else:
        return resposta(404, False, "Rota não encontrada")

except Exception as e:
    return resposta(500, False, str(e))
Enter fullscreen mode Exit fullscreen mode

Enter fullscreen mode Exit fullscreen mode

Top comments (0)