API Gateway’s proxy integration for Lambda is the default, and it’s the magic that makes serverless APIs feel, well, serverless.

Here’s a Lambda function:

import json

def lambda_handler(event, context):
    print(f"Received event: {json.dumps(event)}")
    return {
        'statusCode': 200,
        'headers': {
            'Content-Type': 'application/json'
        },
        'body': json.dumps({
            'message': 'Hello from Lambda!',
            'input': event # Show what API Gateway sent
        })
    }

And here’s how you’d set up API Gateway to call it using proxy integration. In the AWS console, create a new REST API. Then, create a GET method on the root resource (/). When prompted for integration type, choose Lambda Function. Crucially, check the box for Use Lambda Proxy integration. For the Lambda Function, select the lambda_handler function you just created. API Gateway will prompt you to grant permission for it to invoke your Lambda function.

When a request hits API Gateway, say curl https://<api-id>.execute-api.<region>.amazonaws.com/stage/myresource, API Gateway constructs a specific JSON event object and sends it to your Lambda function. Your Lambda function receives this event object, which contains everything about the HTTP request: headers, query parameters, path parameters, the request body, and more.

{
  "resource": "/{proxy+}",
  "path": "/myresource",
  "httpMethod": "GET",
  "headers": {
    "Accept": "*/*",
    "Authorization": "Bearer <token>",
    "CloudFront-Forwarded-For": "70.132.11.22, 54.240.196.186",
    "CloudFront-Viewer-Country": "US",
    "Host": "<api-id>.execute-api.<region>.amazonaws.com",
    "User-Agent": "curl/7.64.1",
    "Via": "1.1 03f8c779b9325f091812c71f217d0100.cloudfront.net (CloudFront)",
    "X-Amz-Cf-Id": "...",
    "X-Amz-Cf-Pop": "LAX5",
    "X-Amzn-Trace-Id": "Root=1-...",
    "X-Forwarded-For": "70.132.11.22, 54.240.196.186",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https"
  },
  "multiValueHeaders": {
    "Accept": ["*/*"],
    "Authorization": ["Bearer <token>"],
    "CloudFront-Forwarded-For": ["70.132.11.22, 54.240.196.186"],
    "CloudFront-Viewer-Country": ["US"],
    "Host": ["<api-id>.execute-api.<region>.amazonaws.com"],
    "User-Agent": ["curl/7.64.1"],
    "Via": ["1.1 03f8c779b9325f091812c71f217d0100.cloudfront.net (CloudFront)"],
    "X-Amz-Cf-Id": ["..."],
    "X-Amz-Cf-Pop": ["LAX5"],
    "X-Amzn-Trace-Id": ["Root=1-..."],
    "X-Forwarded-For": ["70.132.11.22, 54.240.196.186"],
    "X-Forwarded-Port": ["443"],
    "X-Forwarded-Proto": ["https"]
  },
  "queryStringParameters": {
    "key1": "value1",
    "key2": "value2"
  },
  "multiValueQueryStringParameters": {
    "key1": ["value1"],
    "key2": ["value2"]
  },
  "pathParameters": null,
  "stageVariables": null,
  "requestContext": {
    "resourceId": "...",
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "extendedRequestId": "...",
    "requestTime": "01/Jan/2023:12:34:56 +0000",
    "path": "/myresource",
    "accountId": "...",
    "protocol": "HTTP/1.1",
    "stage": "stage",
    "domainPrefix": "<api-id>",
    "requestTimeEpoch": 1672534496000,
    "requestId": "...",
    "identity": {
      "cognitoIdentityPoolId": null,
      "accountId": null,
      "cognitoIdentityId": null,
      "caller": null,
      "sourceIp": "70.132.11.22",
      "principalOrgId": null,
      "accessKey": null,
      "cognitoAuthenticationType": null,
      "cognitoAuthenticationProvider": null,
      "userArn": null,
      "userAgent": "curl/7.64.1",
      "user": null
    },
    "domainName": "<api-id>.execute-api.<region>.amazonaws.com",
    "apiId": "<api-id>"
  },
  "body": null,
  "isBase64Encoded": false
}

Your Lambda function then must return a specific JSON structure that API Gateway understands. This structure dictates the HTTP response: statusCode, headers, and body. The body must be a string, and if it’s JSON, it should be stringified. isBase64Encoded can be set to true if your body is binary data.

The "proxy" part means API Gateway doesn’t try to map request parameters to Lambda arguments or transform the response. It’s a direct pass-through. You get the raw HTTP request details in the event object, and you return the raw HTTP response details in your return value. This simplifies things immensely, as you don’t need to configure mappings or worry about API Gateway mangling your data.

The most surprising thing about proxy integration is that the structure of the event object is largely dictated by the API Gateway service itself, not a generic Lambda event schema. This means if you switch from API Gateway to another service like Application Load Balancer (ALB) or directly invoke your Lambda, the event object will be different, and your Lambda code will need to adapt. You’re not writing truly portable Lambda code in this sense, but rather code tailored to the specific AWS service invoking it.

When you set up your API Gateway method, you’re essentially telling API Gateway, "For any request that matches this method (e.g., GET on /), pass the entire request details as a JSON event object to this specific Lambda function. Then, take the JSON response from that Lambda function and use its statusCode, headers, and body to construct the HTTP response back to the client."

This model is incredibly powerful because it allows you to build complex APIs with minimal configuration in API Gateway. You can handle authentication, authorization, request validation, and response transformation all within your Lambda function. You can also use wildcards in your API Gateway resource paths (like /{proxy+}) to route all requests under a certain path to a single Lambda function, which then acts as a full-fledged application router.

The one thing most people don’t realize is how much control they have over the exact content of the event object through API Gateway’s configuration, particularly with request/response transformations, even though proxy integration is meant to be a pass-through. While the default proxy integration passes the raw event, you can define mapping templates for specific request and response scenarios before the Lambda is invoked or after it returns, which can subtly alter the event your Lambda sees or the response it returns. However, for pure proxy integration, these are typically left empty, and the raw event is passed.

Once you’ve mastered proxy integration, the next logical step is exploring how to handle different HTTP methods (POST, PUT, DELETE) and more complex routing with path parameters and custom authorizers to secure your API.

Want structured learning?

Take the full Apigateway course →