skip to content

REST vs HTTP APIs & Integrations

The three API Gateway flavors — REST, HTTP, and WebSocket — and how each wires a route to a backend. Interviewers ask which you would pick and why, since HTTP APIs are cheaper and faster but drop features teams often assume are always there.

part ofAWSoverview, primer and where to startread it →
on this pageshow

questions

6

An API Gateway route uses the Lambda proxy (`AWS_PROXY`) integration and every call returns HTTP 502 with `{"message": "Internal server error"}`, yet the function's own CloudWatch logs show it completing without error. What is wrong, and what must a Lambda proxy handler return?

level: juniorimportance: must knowfreq 60%

answer

  1. proxy means no transformation layer
  2. the return value IS the response
  3. check the shape, not the logic
  4. body must already be a string
  5. statusCode number, body string, isBase64Encoded

basics

~20 s

The function returned a shape API Gateway cannot map to an HTTP response. A Lambda proxy handler must return an object with a numeric statusCode, an optional headers map, and a body that is already a string — returning a raw object or a bare value produces 502.

solid answer

~50 s

With the `AWS_PROXY` integration, API Gateway does no transformation: whatever the function returns *is* the HTTP response, so it must match a fixed shape — `statusCode` as a number, optional `headers` (and `multiValueHeaders` on REST APIs), `body` as a **string**, and `isBase64Encoded` for binary payloads. If the function returns a plain object, forgets `statusCode`, or leaves `body` as an object instead of `JSON.stringify`-ing it, API Gateway cannot build a response and answers 502 with the generic `Internal server error` message — which is why the function's logs look clean. The fix is to stringify the body and return the envelope. One wrinkle: on HTTP APIs using payload format 2.0, a function that returns valid JSON with no `statusCode` is treated as a 200 with that JSON as the body, so the same handler can "work" on an HTTP API and 502 on a REST API.

code

javascript · 17 lines
javascript
// Correct AWS_PROXY response envelope (payload format 1.0)
export const handler = async (event) => {
  try {
    const id = event.pathParameters?.id;
    if (!id) {
      return { statusCode: 400, body: JSON.stringify({ error: "missing id" }) };
    }
    return {
      statusCode: 200,
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ id })   // string, not an object
    };
  } catch (err) {
    console.error(err);
    return { statusCode: 500, body: JSON.stringify({ error: "internal" }) };
  }
};

go deeper

for a junior

Memorise the envelope: numeric statusCode, optional headers, and a body you have already stringified. Say plainly that with proxy integration your return value is the HTTP response.

for a middle

Explain that no transformation layer exists in AWS_PROXY, and separate the failure modes: 502 for malformed or thrown, 504 for integration timeout, 500 with AccessDeniedException for invoke permission.

for a senior

Demonstrate the diagnosis path — execution logs or $context.integrationErrorMessage in the access log — and show the codified error envelope you would put in a shared handler wrapper so callers never see an opaque 502.

for a principal

Own the platform contract: one shared response/error helper across services, a house standard on payload format, and log fields chosen so an operator can tell gateway faults from backend faults without reading code.

## What proxy integration actually means API Gateway integrations come in two families. A **non-proxy** (custom) integration lets you write mapping templates that translate the HTTP request into whatever the backend wants, and translate the backend's answer back into an HTTP response. A **proxy** integration deliberately removes that layer: the whole request is handed to the backend as a structured event, and the backend's return value is taken as the whole response. For Lambda that integration type is `AWS_PROXY`. That trade is the source of this failure. Because there is no mapping template to fix things up, the contract on the return value is exact, and any deviation is unmappable. ## The required response shape For the REST-era payload format (format 1.0), the handler must return an object with: - `statusCode` — a **number**, required. - `headers` — optional object of single-valued headers. - `multiValueHeaders` — optional object whose values are arrays, for headers that repeat (`Set-Cookie`). - `body` — a **string**. Not an object, not a number. - `isBase64Encoded` — boolean; when `true`, `body` is base64 and API Gateway decodes it into binary bytes (the media type must also be listed in the API's binary media types). ```javascript export const handler = async (event) => ({ statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: event.pathParameters.id }) }); ``` Return `{ statusCode: 200, body: { id: 1 } }` and you get 502, because `body` is an object. Return `{ id: 1 }` and you get 502, because there is no `statusCode`. ## Why the logs look innocent The function *succeeded*. It ran, logged, and returned. The failure happens afterwards, inside API Gateway, when it tries to turn that return value into an HTTP response. That is exactly why the generic `Internal server error` body is so unhelpful and why the debugging move is to look at the **API Gateway execution logs** (or the access log with `$context.integrationErrorMessage`), not at the function logs. Execution logs will say the response was malformed. ## The 502 / 504 / 500 triage Worth internalising, because interviewers push on it: - **502 Bad Gateway** — the function threw an unhandled error, or returned a shape API Gateway cannot map. - **504 Gateway Timeout** — the integration exceeded API Gateway's limit (tens of seconds), even if the function itself keeps running to its own longer timeout. Raising the Lambda timeout does not help. - **403** — usually authorization or a resource-policy denial, before the integration is reached at all. - **500 with `AccessDeniedException` in the execution log** — API Gateway is not permitted to invoke the target function. ## Payload format 1.0 versus 2.0 REST APIs always use format 1.0. HTTP APIs default to **2.0** and let you pin 1.0. Two differences bite people: 1. The **event** is flatter in 2.0: `event.requestContext.http.method` rather than `event.httpMethod`, `event.rawPath`, `event.rawQueryString`, `cookies` as an array rather than folded into headers. A handler written for a REST API often breaks when copied to an HTTP API. 2. The **response** is more forgiving in 2.0: if the function returns valid JSON without a `statusCode`, API Gateway infers `200` and uses the JSON as the body with `application/json`. Convenient, but it means the strict envelope is a habit you should keep, because the identical handler behind a REST API 502s. ## When to use non-proxy instead Proxy integration puts the HTTP contract in your code, which is usually what you want: the handler is testable, the API definition is thin, and there is no VTL to maintain. Non-proxy integration earns its keep when the backend cannot be changed to speak the envelope — a legacy service, or an `AWS` integration that calls another service directly with no Lambda at all. The price is mapping templates written in Velocity Template Language, plus integration responses that select an HTTP status by regex-matching the error string the backend returned. Teams routinely underestimate how unpleasant that is to own and how hard it is to unit-test, which is why proxy integration is the default recommendation. ## The habit that prevents the bug Wrap every handler's exit in a small helper that builds the envelope, and never return a raw value from business logic. Then a thrown error becomes a deliberate 4xx/5xx envelope with a useful body, instead of an unhandled exception that surfaces to the caller as an opaque 502.

  • How do you get a useful error message instead of the generic `Internal server error`?
    Turn on API Gateway execution logging for the stage, or add `$context.integrationErrorMessage` and `$context.error.message` to the access log format. Those tell you whether the response was malformed, the invocation failed, or permission was denied. Then handle errors inside the function so the caller receives a deliberate status and body rather than an unhandled exception.
  • The same handler returns 502 behind a REST API but 200 behind an HTTP API. Why?
    HTTP APIs default to payload format 2.0, where a function returning valid JSON without a `statusCode` is interpreted as a 200 with that JSON as the body. REST APIs use format 1.0 and require the full envelope, so the missing `statusCode` is unmappable and becomes 502. Always return the explicit envelope so behaviour does not depend on the API type.
  • When would you deliberately use a non-proxy Lambda integration instead?
    When the backend's contract cannot change — a legacy function or another AWS service you are calling directly with no Lambda at all — and you need mapping templates to reshape request and response. You pay for it in VTL you must maintain and integration responses that pick a status code by regex-matching the error string, which is much harder to test than code.

saying these in an interview costs you the question

  • Blames the function's business logic when the shape is wrong
  • Returns the body as an object instead of a string
  • Thinks a 502 always means the backend crashed
  • Assumes raising the Lambda timeout cures a 504
  • Believes proxy integration still applies mapping templates

context

open as a page

Amazon API Gateway offers REST APIs and HTTP APIs (as well as WebSocket APIs). What would make you choose a REST API over an HTTP API, and what are you giving up if you default to HTTP APIs?

level: middleimportance: must knowfreq 70%

basics

~20 s

HTTP APIs are cheaper and lower-latency, so default to them for plain Lambda or HTTP backends. Choose REST APIs when you need what HTTP APIs omit: API keys and usage plans, stage response caching, request validation, VTL mapping templates, private endpoints, or AWS WAF.

open as a page

With an Amazon API Gateway WebSocket API, how does your backend send a message to a client after that client has connected, and what must happen on the `$connect` route for it to be possible?

level: middleimportance: should knowfreq 40%

basics

~20 s

API Gateway gives every connection a connectionId, which your $connect handler must persist (typically in DynamoDB) alongside the user it belongs to. The backend then pushes by calling PostToConnection on the API's @connections management endpoint, needing the execute-api:ManageConnections permission.

open as a page

In Amazon API Gateway, what is the difference between a private API and a private integration, and what mechanism implements each one?

level: seniorimportance: should knowfreq 45%

basics

~20 s

They control opposite directions. A private API restricts who may call the API: it is a REST API with the PRIVATE endpoint type, reachable only through an interface VPC endpoint for execute-api. A private integration is how the API reaches a backend inside a VPC, using a VPC link.

open as a page

In an API Gateway REST API, what are stages and stage variables, and what commonly breaks when a stage variable selects which Lambda alias the stage invokes?

level: seniorimportance: should knowfreq 33%

basics

~20 s

A stage is a named, invocable snapshot of a deployment (dev, prod), and stage variables are per-stage key/value pairs referenced as ${stageVariables.name} inside integration URIs and mapping templates. When a variable picks the Lambda alias, API Gateway cannot add the invoke permission for you, so calls fail with 500 until you add it per alias.

open as a page

You want an API Gateway API served at https://api.example.com instead of its default execute-api URL. What do you configure, and why does the endpoint type decide which region the ACM certificate must live in?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Create an API Gateway custom domain name with an ACM certificate, add a base-path/API mapping to a specific API and stage, then point DNS at the target domain the service returns. An edge-optimized domain is fronted by CloudFront, so its certificate must be in us-east-1; a regional domain needs the certificate in the API's own region.

open as a page