The biggest surprise about migrating from API Gateway v1 REST APIs to v2 HTTP APIs is that you’re not just upgrading; you’re fundamentally switching to a new, cheaper, and faster underlying service that uses different protocols and paradigms.

Let’s see it in action. Imagine we have a simple v1 REST API that proxies requests to a Lambda function.

V1 REST API Configuration (Conceptual):

{
  "paths": {
    "/items": {
      "get": {
        "summary": "Get all items",
        "operationId": "getAllItems",
        "responses": {
          "200": {
            "description": "A list of items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {"type": "string"},
                      "name": {"type": "string"}
                    }
                  }
                }
              }
            }
          }
        },
        "x-amazon-apigateway-integration": {
          "type": "aws_proxy",
          "integrationHttpMethod": "POST",
          "uri": "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-lambda-function/invocations",
          "credentials": "arn:aws:iam::123456789012:role/apigateway-execution-role"
        }
      }
    }
  }
}

And here’s the corresponding Lambda function (Node.js):

exports.handler = async (event) => {
    console.log(JSON.stringify(event, null, 2));
    // v1 expects a specific format for integration response
    return {
        statusCode: 200,
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify([
            { id: "1", name: "Widget" },
            { id: "2", name: "Gadget" }
        ])
    };
};

Now, let’s look at the v2 HTTP API equivalent. We’ll use the AWS CLI for clarity, as the console can abstract away some details.

V2 HTTP API Creation (AWS CLI):

  1. Create the HTTP API:

    aws apigatewayv2 create-api --name "my-http-api" --protocol-type HTTP --target arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-lambda-function/invocations
    

    This command creates the API and implicitly sets up a default integration to the specified Lambda function. The target parameter here is a bit of a shortcut; typically you’d create an integration and then a route pointing to it.

  2. Get the API ID: You’ll get an output like:

    {
        "apiId": "abcdef123",
        "apiEndpoint": "https://abcdef123.execute-api.us-east-1.amazonaws.com",
        "name": "my-http-api",
        "protocolType": "HTTP",
        "description": "",
        "createdDate": "2023-10-27T10:00:00Z",
        "tags": {}
    }
    

    Note the apiEndpoint. This is your new public URL.

  3. Create a Lambda Integration (if not done by default):

    aws apigatewayv2 create-integration --api-id abcdef123 --integration-type AWS_PROXY --integration-method POST --payload-format-version 2.0 --integration-uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-lambda-function/invocations
    

    The --payload-format-version 2.0 is crucial. This tells API Gateway to use the newer, more streamlined payload format for Lambda.

  4. Create a Route:

    aws apigatewayv2 create-route --api-id abcdef123 --route-key "GET /items" --target integrations/YOUR_INTEGRATION_ID
    

    Replace YOUR_INTEGRATION_ID with the ID returned from the create-integration command. The route-key is a direct mapping of HTTP method and path.

  5. Deploy the API:

    aws apigatewayv2 create-deployment --api-id abcdef123 --description "Initial deployment"
    

    You’ll need to create a deployment stage to make it accessible.

  6. Create a Stage:

    aws apigatewayv2 create-stage --api-id abcdef123 --stage-name prod --auto-deploy
    

V2 HTTP API Lambda Function (Node.js):

exports.handler = async (event) => {
    console.log(JSON.stringify(event, null, 2));
    // v2 payload format is simpler
    // event.requestContext.http.method, event.rawPath, event.queryStringParameters, etc.
    // For response, just return the body and statusCode. Headers are handled differently.
    return {
        statusCode: 200,
        body: JSON.stringify([
            { id: "1", name: "Widget" },
            { id: "2", name: "Gadget" }
        ])
    };
};

Notice the Lambda response structure: it’s simpler for v2. The event object passed to Lambda is also different; v2 uses a more unified, JSON-native structure.

The core problem this solves is the overhead and cost associated with the v1 REST API service. V1’s architecture is built around the OpenAPI Specification (Swagger) and has more features like request/response transformation, custom authorizers (which are more complex in v1), and API keys that come with a performance and cost penalty. V2 HTTP APIs are designed for pure HTTP proxying and Lambda integration, shedding much of that complexity. They offer significantly lower latency and a much lower price per million requests (e.g., $1.00/million for v2 vs. $3.50/million for v1).

You build v2 APIs using a combination of create-api, create-integration, create-route, and create-deployment commands (or their console equivalents). The route-key directly maps HTTP methods and paths (e.g., GET /users/{userId}). Integrations can be Lambda, HTTP (for external endpoints), or AWS services. The --payload-format-version 2.0 flag on the integration is key for Lambda integrations, as it dictates the structure of the event object your Lambda function receives and the response it expects.

The ability to use ANY as a route key (e.g., ANY /{proxy+}) is a powerful simplification for proxying all requests to a backend, similar to v1’s /{proxy+} resource but more direct. You can define multiple routes, and API Gateway matches them based on specificity and the order in which they are defined if there are overlaps. For example, GET /items/{id} would be matched before a more general GET /items/{proxy+} if both existed.

The biggest shift in thinking is that v2 is not just a newer version of the same thing; it’s a distinct product. You lose some of the intricate customization options of v1 (like detailed request mapping templates for non-proxy integrations) but gain simplicity, speed, and cost savings. For most common use cases, especially those proxying directly to Lambda or simple HTTP endpoints, v2 is the clear winner.

When you migrate, you’ll find that v2 automatically handles many of the "behind-the-scenes" steps that v1 required explicit configuration for, such as setting up a default ANY route to a Lambda function if you use the --target parameter during API creation. This makes initial setup much faster.

Want structured learning?

Take the full Apigateway course →