# Building a production HTTP API with Lambda: choosing among API Gateway (REST/HTTP API), Function URLs, and ALB, plus auth, validation, and error design

> An implementation guide to building an HTTP/REST API with AWS Lambda at production quality. It selects among the four entrances — API Gateway REST API, HTTP API, Lambda Function URLs, and ALB — by feature/price/latency, and explains, with real code faithful to the AWS official specs, payload-format-2.0 response inference, JWT/Lambda/IAM authorizers, request validation, CORS, error mapping, and throttling.

- Published: 2026-06-27
- Author: 友田 陽大
- Tags: AWS, Lambda, API Gateway, サーバーレス, アーキテクチャ設計
- URL: https://tomodahinata.com/en/blog/aws-lambda-api-gateway-function-urls-rest-api-production-guide
- Category: AWS Lambda in production
- Pillar guide: https://tomodahinata.com/en/blog/aws-lambda-production-guide

## Key points

- Lambda's front end is a 4-way choice: HTTP API (cheap, fast, built-in JWT) / REST API (validation, API keys, WAF, X-Ray) / Function URLs (minimal setup, prototype) / ALB (ride along on an existing ALB).
- HTTP API is lower cost than REST API ($1.00 vs $3.50 per million) and lower latency. Use REST API only when you need WAF, request validation, API keys, or X-Ray.
- Payload format 2.0 auto-infers 200 / application/json when the function returns valid JSON without statusCode. 1.0 requires the explicit {statusCode, headers, body}.
- Auth is enforced at the entrance, not the UI: HTTP API's JWT authorizer (iss/aud validation, Cognito support) or a Lambda authorizer; service-to-service is IAM (SigV4).
- Integration timeout is REST 29s (Regional/Private can be raised) / HTTP API 30s fixed. API Gateway's max payload is 10MB while Lambda synchronous is 6MB.

---

"I want to build an API with Lambda. But do I put API Gateway in front, or is a Function URL enough, and what's the difference between REST API and HTTP API anyway?" — when shipping a serverless HTTP API to production, this is the fork you always stop at first. Get it wrong and you'll either **keep paying fees you don't need to**, or **have to rebuild because a feature you later needed (WAF, request validation, API keys) is missing.**

This article is an implementation guide for building a **production-quality HTTP/REST API** with AWS Lambda. It explains end-to-end from **selecting the four entrances** through payload format, auth, validation, CORS, error design, and throttling. As material, it also weaves in the production-operation judgment of `API Gateway → … → 221 API endpoints` from the lumber-distribution B2B SaaS that won the Minister of Economy, Trade and Industry Award ([lumber-industry DX](/case-studies/lumber-industry-dx)). The Lambda execution model, idempotency, and observability are left to the sister article [AWS Lambda production-operations guide](/blog/aws-lambda-production-guide); this article concentrates on the **single point of "the HTTP entrance."**

> **Rules for this article**: specs, parameter names, pricing, and quotas are based on the **AWS official documentation (as of June 2026).** Pricing changes by region and period (this article uses the published values for US East (N. Virginia), standard tier), and quotas are revised. Before production rollout, always confirm the latest values in the official docs (the "References" at the end).

---

## 0. Mental model: the HTTP entrance is a "feature ↔ cost" trade-off

There are four ways to route HTTP in front of Lambda. They line up on a single axis: **the more features, the more expensive; the simpler, the cheaper.**

- **HTTP API (API Gateway v2)**: cheap, fast, built-in JWT authorizer. **The favorite that covers most APIs with minimal features.**
- **REST API (API Gateway v1)**: request validation, API keys/usage plans, WAF, private endpoints, X-Ray, cache, gateway responses. **Only when you need the "missing features."**
- **Function URLs**: an HTTPS endpoint directly attached to the function without API Gateway in between. For **prototypes, internal tools, and simple webhook receivers.**
- **ALB + Lambda target**: when you already operate an ALB and want to route some paths/headers to Lambda.

> **The official guidance**: AWS recommends Function URLs for "when you want to minimize cost and complexity for **a simple app or prototype**," and API Gateway for "**production, scaling apps.**" When in doubt, remember **HTTP API is the default correct answer** and you won't go far wrong.

---

## 1. Choosing the four entrances: a quick reference of feature, price, and latency

First, fix it with a selection table. This is the core of this article's decision-making.

| Aspect | HTTP API | REST API | Function URLs | ALB + Lambda |
| --- | --- | --- | --- | --- |
| **Price (1M requests)** | $1.00 (~300M) / then $0.90 | from $3.50 (top tier $1.51) | no extra charge (Lambda billing only) | ALB hour/LCU billing |
| **Latency** | low (up to 60% less than REST) | standard | minimal (direct) | low |
| **JWT authorizer** | ✅ built-in | ❌ (substitute with Cognito authorizer) | ❌ (IAM or public) | ❌ (ALB's OIDC auth) |
| **Request validation** | ❌ | ✅ | ❌ | ❌ |
| **API keys / usage plans** | ❌ | ✅ | ❌ | ❌ |
| **WAF** | ❌ | ✅ | ❌ (combine with CloudFront) | ✅ |
| **X-Ray tracing** | ❌ | ✅ | (Lambda side only) | (Lambda side only) |
| **Private / edge-optimized** | ❌ | ✅ | ❌ | inside VPC |

The selection logic is simple.

1. **Do you need any of WAF, request validation, API keys (usage plans), X-Ray, or a private API?** → **REST API.**
2. **Don't need them (most APIs are here)?** → **HTTP API.** Cheap and fast.
3. **A simple entrance that doesn't need API Gateway features at all?** → **Function URLs.**
4. **Want to ride along under an existing ALB?** → **ALB + Lambda.**

> **REST API's price is more than 3× HTTP API's** ($3.50 vs $1.00 per million, first tier). Starting with "REST API for now" and using neither WAF nor validation nor API keys is **the most common waste.** Choose by working backward from the features you use.

---

## 2. Proxy integration and payload format: the decisive difference between 1.0 and 2.0

API Gateway, with **Lambda proxy integration**, stuffs the whole request into `event` and passes it to the function, and makes the function's return value the response. Here, if you don't understand HTTP API's **payload format version**, you'll get stuck.

### 2.1 Format 2.0's "automatic response inference"

The important official spec: **in format 2.0, if the function returns valid JSON not containing `statusCode`, API Gateway auto-infers `statusCode: 200`, `content-type: application/json`, `isBase64Encoded: false`, `body: the return value`.** In other words, **the handler can just return the object as-is.**

```ts
// HTTP API（ペイロード形式 2.0）：オブジェクトを返すだけで 200 application/json になる
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda";

export const handler = async (
  event: APIGatewayProxyEventV2,
): Promise<APIGatewayProxyStructuredResultV2 | { ok: boolean; path: string }> => {
  // 2.0 では method/path は requestContext.http に入る（1.0 の httpMethod/path とは別物）
  const { method, path } = event.requestContext.http;
  if (method !== "GET") {
    // 明示したいときは従来どおり statusCode を返せる（推論より優先される）
    return { statusCode: 405, body: JSON.stringify({ message: "Method Not Allowed" }) };
  }
  // statusCode を省略 → 200 / application/json に自動推論される
  return { ok: true, path };
};
```

On the other hand, **in format 1.0 (REST API, or an HTTP API where you explicitly chose 1.0), you must always return the `{ statusCode, headers, body }` envelope.** If the envelope is broken, API Gateway returns **502 Bad Gateway.**

| Item | Format 1.0 | Format 2.0 |
| --- | --- | --- |
| Method/path | `event.httpMethod` / `event.path` | `event.requestContext.http.method` / `.path` |
| Multi-value headers | has `multiValueHeaders` | abolished (comma-joined into `headers`) |
| Cookie | none | `event.cookies` array (response too via `cookies`) |
| Response | `{statusCode, headers, body}` required | infers 200/JSON when omitted |

> **The crux of design**: a new HTTP API **defaults to format 2.0** (the console selects the latest by default). However, **when you create it with CLI/CloudFormation/SDK, not making `payloadFormatVersion` explicit** can drift from your intent. As for types, **don't mix up** `APIGatewayProxyEventV2` (2.0) / `APIGatewayProxyEvent` (1.0) from `@types/aws-lambda` — this is a quietly common source of bugs.

---

## 3. Authentication / authorization: enforce at the "entrance," not the UI

Auth starts from "don't trust the client." The iron rule is to **reject at the API's entrance (the authorizer), not in a UI if-statement.** The mechanisms available differ by entrance.

### 3.1 JWT authorizer (HTTP API only, the strongest)

HTTP API has a **built-in JWT authorizer**, which validates OAuth2/OIDC JWTs (e.g., issued by a Cognito user pool) down to the **signature, issuer (`iss`), and audience (`aud`).** You can enforce auth at the entrance **without writing a single line of code** — this is one of the biggest reasons to choose HTTP API.

```hcl
# Terraform: HTTP API の JWT オーソライザー（Cognito ユーザープールを発行者に）
resource "aws_apigatewayv2_authorizer" "jwt" {
  api_id           = aws_apigatewayv2_api.http.id
  authorizer_type  = "JWT"
  identity_sources = ["$request.header.Authorization"]
  name             = "cognito-jwt"

  jwt_configuration {
    audience = [aws_cognito_user_pool_client.app.id]                                  # aud を検証
    issuer   = "https://cognito-idp.${var.region}.amazonaws.com/${aws_cognito_user_pool.main.id}" # iss を検証
  }
}
```

The pitfalls when implementing the internals of JWT validation yourself (fetching JWKS, RS256, kid, expiry) are detailed in the sister article [the correct implementation of Cognito JWT (RS256) verification](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide).

### 3.2 Other authorizers

| Mechanism | Usable entrance | Use |
| --- | --- | --- |
| **JWT authorizer** | HTTP API | OIDC/OAuth2 JWT. Enforce auth at minimal cost |
| **Lambda authorizer** | REST / HTTP API | custom tokens, complex authorization logic. REST = TOKEN/REQUEST, HTTP = REQUEST |
| **Cognito authorizer** | REST API | Cognito user pool (`COGNITO_USER_POOLS`) |
| **IAM authorization (SigV4)** | REST / HTTP API / Function URL | service-to-service, internal clients. Controlled with `execute-api` permission |

The **Lambda authorizer** can cache results (REST API's default TTL is **300 seconds**, max 3600 seconds, `0` to disable). It's an important parameter so as not to run heavy validation on every auth. For **service-to-service communication**, **IAM authorization (SigV4 signing)** is safer than a token — no key management is needed, and you can express least privilege with IAM.

---

## 4. Request validation: reject at the boundary (REST API's strength)

**Validate all external input at the boundary** — this is a security basic. REST API, with **models (JSON Schema) and request validators**, can **reject with 400** an invalid body or a missing required parameter before it reaches the function. Since you can drop the invalid without using even 1ms of Lambda execution time, it also helps cost.

HTTP API lacks this feature, so **validate inside the handler.** To reject type-safely, place a schema like `zod` at the boundary.

```ts
// HTTP API：リクエスト検証は関数の入口で。境界でparse→失敗は422で即返す。
import { z } from "zod";
import type { APIGatewayProxyEventV2 } from "aws-lambda";

const CreateOrder = z.object({
  itemId: z.string().min(1),
  quantity: z.number().int().positive(),         // 正の整数のみ許可
  note: z.string().max(500).optional(),
});

export const handler = async (event: APIGatewayProxyEventV2) => {
  const parsed = CreateOrder.safeParse(JSON.parse(event.body ?? "{}"));
  if (!parsed.success) {
    // 何が不正かをクライアントに返す（PIIは載せない）
    return { statusCode: 422, body: JSON.stringify({ errors: parsed.error.flatten() }) };
  }
  return await createOrder(parsed.data); // 以降は型安全な data を扱う
};
```

> **The crux of DRY**: don't **double-manage** the REST API model and the front-end/function types. Ideally, make either JSON Schema or Zod the single source of truth and generate the other. If you copy-paste the validation logic into both "the entrance (API Gateway)" and "the function," they'll always drift.

---

## 5. Limits and timeouts: numbers that bite in production if you don't know them

The **mismatch in limits** between API Gateway and Lambda is a typical cause of production incidents. Fix it with a table.

| Item | Value (official) | Note |
| --- | --- | --- |
| **Integration timeout (REST)** | 50ms–**29s** | Regional/Private can be raised (cap undisclosed, by request). Edge-optimized is fixed at 29s |
| **Integration timeout (HTTP API)** | up to **30s** (fixed) | not raisable |
| **Max payload (API Gateway)** | **10 MB** | not raisable |
| **Lambda synchronous response** | **6 MB** | smaller than API Gateway's 10MB. Large responses need splitting / a presigned URL |
| **Default throttle** | 10,000 RPS + burst 5,000 | some 13 regions are 2,500 / 1,250. Shared across HTTP/REST/WebSocket |
| **Function URL throttle** | reserved concurrency × 10 RPS | over is 429. `reserved=0` for full stop (kill switch) |

How they bite in practice:

- **The 29s/30s wall**: heavy synchronous processing (report generation, external-API aggregation) should be **made asynchronous** before hitting this wall. Return 202 immediately and poll/WebSocket for the result, or use Step Functions. If long-running processing is genuinely needed, revisit the [Fargate vs Lambda selection](/blog/aws-ecs-fargate-vs-lambda-vs-app-runner-compute-selection-guide).
- **6MB vs 10MB**: Lambda's cap is 6MB. Design large responses to **return an S3 presigned URL** (Lambda response streaming allows up to 200MB but via a Function URL / dedicated API).
- **Throttle**: the official explicitly says of usage-plan throttle/quota that you should "**not rely on it for cost control or blocking access** (it's not a hard limit)." **Build the fortress of a cost ceiling with reserved concurrency and WAF rate limiting.**

---

## 6. CORS and error design: kind to the browser and to operations

### 6.1 Manage CORS centrally at the "entrance"

- **HTTP API**: CORS is **configured per API**, and preflight (OPTIONS) is **answered automatically by API Gateway.** **CORS headers the backend returns are ignored.** So adding CORS headers on the function side is futile — manage centrally via configuration.
- **REST API (proxy integration)**: **the backend (function) must return `Access-Control-Allow-*`.** Non-proxy returns OPTIONS via a mock.
- **Function URL**: use the built-in CORS configuration (`AllowOrigins`, etc.). Configuration is recommended over hand-written headers.

```hcl
# HTTP API：CORSはAPI単位で宣言。ワイルドカード '*' を本番で使わず、許可originを列挙する
resource "aws_apigatewayv2_api" "http" {
  name          = "orders-api"
  protocol_type = "HTTP"
  cors_configuration {
    allow_origins = ["https://app.example.com"]   # 信頼するoriginだけ
    allow_methods = ["GET", "POST"]
    allow_headers = ["authorization", "content-type"]
    max_age       = 600
  }
}
```

### 6.2 Errors with the correct status, without leaking information

- **REST API**: the 4xx/5xx that API Gateway itself produces (auth denial → 403, integration timeout → 504, throttle → 429) can be customized with **gateway responses** (REST API only).
- **Mapping Lambda errors**: with proxy integration, **the function returns `statusCode`** (4xx = client, 5xx = server). Non-proxy must map with `selectionPattern` regex, or **even when the function throws an exception, it becomes the default 200.**
- **Prevent information leakage**: don't put the stack trace or internal messages directly in the body. Return only a correlation ID (`context.awsRequestId`) to the client, and put details in structured logs (see [the production-operations guide for observability](/blog/aws-lambda-production-guide)).

```ts
// 例外を「クライアント向けの安全なレスポンス」に翻訳する薄い層
class HttpError extends Error {
  constructor(readonly status: number, readonly publicMessage: string) { super(publicMessage); }
}

export const handler = async (event: APIGatewayProxyEventV2) => {
  try {
    return await route(event);
  } catch (err) {
    if (err instanceof HttpError) {
      return { statusCode: err.status, body: JSON.stringify({ message: err.publicMessage }) };
    }
    // 想定外は500。内部詳細はログにだけ出し、bodyには相関IDのみ
    console.error(JSON.stringify({ level: "ERROR", requestId: event.requestContext.requestId, err: `${err}` }));
    return { statusCode: 500, body: JSON.stringify({ message: "Internal Server Error" }) };
  }
};
```

---

## 7. Observability: mind the difference between HTTP API and REST API

To trace "the API is slow / down," grasp the entrance's metrics and logs. Here too, note that **the features available differ between HTTP API and REST API.**

- **Metrics (common)**: `Count` / `Latency` / `IntegrationLatency` / `4XXError` / `5XXError` in the `AWS/ApiGateway` namespace. **The difference between `Latency` (the whole) and `IntegrationLatency` (the Lambda side)** is API Gateway's own overhead.
- **Logs**: REST API has **both execution logs and access logs.** HTTP API has **access logs only.** Emit `$context.requestId` and `$context.integration.latency` in the access log and cross-reference with the Lambda-side correlation ID (`context.awsRequestId`).
- **X-Ray**: **REST API only** (HTTP API not supported). If you want end-to-end traces from the entrance, this is a REST API advantage.

> **Alert on symptoms**: `5XXError`, `4XXError` (a spike is an auth/validation incident), `Latency` P99, an abnormal drop in `Count`. If `Latency − IntegrationLatency` swells, it's the API Gateway side; if `IntegrationLatency` swells, it's the Lambda side (including cold starts; [cold-start optimization](/blog/aws-lambda-cold-start-snapstart-provisioned-concurrency-performance-guide)).

---

## 8. Conclusion: a cheat sheet for the serverless API entrance

- **The default entrance is HTTP API**: cheap ($1.00/million), fast (up to 60% lower latency than REST), built-in JWT authorizer.
- **Choose REST API only when there's a feature requirement**: request validation, API keys/usage plans, WAF, X-Ray, private API.
- **Function URLs are for prototypes / simple entrances**, **ALB is for riding along on an existing ALB.**
- **Payload format 2.0**: just return an object for 200/JSON inference. 1.0 requires the envelope. Don't mix up the types (V2 vs V1).
- **Enforce auth at the entrance**: HTTP API's JWT authorizer (iss/aud/Cognito), or IAM (SigV4) for service-to-service.
- **Validate at the boundary**: REST API uses model + validator, HTTP API uses Zod at the function entrance. Don't double-manage.
- **Know the numbers**: integration timeout 29s (REST) / 30s (HTTP), payload 10MB (GW) / 6MB (Lambda synchronous), build the cost ceiling with reserved concurrency + WAF.
- **Manage CORS centrally via configuration, errors with the correct status without leaking info, and observability mindful of the HTTP/REST difference.**

In the lumber-distribution B2B SaaS that won the Minister of Economy, Trade and Industry Award, I operate **221 API endpoints** in production on top of an `API Gateway → NLB → ALB` configuration. By being thorough about entrance selection, centralizing auth, validating at the boundary, and symptom-based alerts, I've balanced the front-end's perceived quality and security.

**"I want to ship my own serverless API to production with a cheap, fast, safe entrance design" — from entrance selection through auth, validation, and observability, I accompany you end-to-end at the speed of one person × generative AI (Claude Code).** From inventorying an existing API (are you paying waste on REST API?) onward too, feel free to reach out.

---

### References (official documentation)

- [Choose between REST APIs and HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html) — feature comparison table (validation, API keys, WAF, X-Ray, JWT, etc.)
- [Working with Lambda proxy integrations for HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html) — payload format 1.0/2.0, automatic response inference
- [Function URLs vs. Amazon API Gateway](https://docs.aws.amazon.com/lambda/latest/dg/furls-http-invoke-decision.html) — using Function URLs vs. API Gateway
- [Using AWS Lambda with an Application Load Balancer](https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html) — ALB target, response format
- [JWT authorizers for HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) / [Lambda authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html) — iss/aud validation, TOKEN/REQUEST, cache TTL
- [Request validation for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-request-validation.html) — model/validator
- [Amazon API Gateway quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html) / [HTTP API quotas](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-quotas.html) — integration timeout, payload, throttle
- [CORS for HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html) / [REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html) — centralized CORS configuration
- [Gateway responses / error handling](https://docs.aws.amazon.com/apigateway/latest/developerguide/handle-errors-in-lambda-integration.html) — mapping Lambda errors, cause of 502
- [Amazon API Gateway pricing](https://aws.amazon.com/api-gateway/pricing/) — HTTP API / REST API pricing
