Beetroot LogoBeetroot

API Gateway HTTP API

Expose the Beetroot API Lambda using an HTTP API in API Gateway with routes, CORS, and a curl test.

Goal

By the end of this phase, you'll have a public HTTP endpoint (API Gateway) that calls your API Lambda.

Your React app will be able to call:

GET /persons
GET /persons/{personId}/photos

Mental model: API Gateway vs Lambda

API Gateway is the “front door” (public URL + routing + CORS). Lambda is the “brain” (runs your Python code and returns JSON).

Prerequisites

  • Lambda: beetroot-api (from the previous phase)

Region reminder

Create the HTTP API in the same region as your Lambda (us-east-1), otherwise the integration won't show up.

Step 1: Create an HTTP API

Create the API

  1. Open API Gateway
  2. Click Create API
  3. Choose HTTP APIBuild

Why HTTP API (not REST API)?

HTTP API is simpler and cheaper for basic GET routes. It's a great fit for this workshop.

Add the Lambda integration

In the setup flow:

  1. API Name: BeetrootAPIGateway
  2. Under Integrations, select Lambda
  3. Region: us-east-1
  4. Choose your Lambda: beetroot-api
  5. Next

What “integration” means

An integration is just “what should run when someone hits this route?”. Here, the answer is your Lambda.

Define routes

MethodResource Path
GET/persons
OPTIONS/persons/{personId}/photos

For both routes, choose the same integration: beetroot-api.

Why both routes use the same Lambda

Your Lambda already routes based on method and path. API Gateway just forwards the request.

Enable CORS (important for React)

In API settings → Develop → CORS:

  • Access-Control-Allow-Origin: * (dev only)
  • Access-Control-Allow-Headers: content-type
  • Access-Control-Allow-Methods: GET, OPTIONS

CORS note

* is fine for development. In production, set this to your real frontend domain.

Stage + deploy

HTTP APIs typically use the $default stage with Auto-deploy. So, there's no need to click on the Deploy button.

What a stage is

A stage is a deployed version of your API. $default keeps things simple for this workshop.

Step 2: Confirm Lambda invoke permission

API Gateway must be allowed to invoke your Lambda. Usually the console adds this automatically, but you can verify by 2 ways.

First, open the beetroot-api lambda function.

  1. You should see API Gateway connected to the lambda function, which gets executed on each request.

beetroot-api function overview with API Gateway

  1. Go to Configuration → Permissions → Resource-based policy statements and look for a statement mentioning apigateway.amazonaws.com

If the permission is missing

If API Gateway can't invoke Lambda, you'll usually get 403 or 500. We'll cover quick fixes in Common issues.

Step 3: Get Invoke URL and test

Go to:

  • API Gateway → BeetrootAPIGatewayStages$default
  • Copy the Invoke URL

Common issues

Most common causes:

  • API Gateway does not have permission to invoke the Lambda
  • You're calling the wrong URL/stage

Check:

  • Lambda → beetroot-apiPermissions includes apigateway.amazonaws.com
  • API Gateway → Stages → using the correct Invoke URL

This usually means the Lambda crashed. Check:

  • CloudWatch logs: /aws/lambda/beetroot-api
  • Common causes:
    • Missing env vars (table names)
    • JSON serialization errors (Decimals) if the previous phase fix wasn't applied

If curl works but the browser fails:

  • Ensure CORS is enabled in API Gateway (Allowed origins/methods/headers)
  • Ensure your Lambda response includes CORS headers (we added them in _resp)

Test

Route 1: list persons

curl -X GET "<INVOKE_URL>/persons"
{
  "persons": [
    {
      "photoCount": 4,
      "createdAt": "2025-12-21T08:16:04.864553+00:00",
      "personId": "a5a48144-1b50-4eca-9b93-65cf88527266",
      "repThumbKey": "faces-thumbs/a5a48144-1b50-4eca-9b93-65cf88527266/9db7214ecc27b77b20eb_face_1.jpg"
    },
    ...
  ]
}

Route 2: photos for one person

Copy one personId from the /persons response.

curl -X GET "<INVOKE_URL>/persons/{personId}/photos"
{
  "personId": "a5a48144-1b50-4eca-9b93-65cf88527266",
  "photos": [
    {
      "photoId": "4fcae858bfb8d5e935bd",
      "photoBucket": "photo-clone-raw",
      "photoKey": "photos-raw/r2h21.jpg",
      "thumbKey": null
    },
    {
      "photoId": "930719d43b6ebef54c53",
      "photoBucket": "photo-clone-raw",
      "photoKey": "photos-raw/r2h1.jpg",
      "thumbKey": null
    },
    ...
  ]
}

What “success” looks like

You should get JSON back with status code 200 (and not 403 / 500). If /persons returns an empty list, it usually means your Persons table is empty (upload more photos first).

On this page