Beetroot LogoBeetroot

API Gateway Endpoint

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 API → Build

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 → BeetrootAPIGateway → Stages → $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-api → Permissions 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