# [AWS] API Gateway
HTTP API のデフォルトルートの挙動を確認してみた


published: 2026-07-25

HTTP API にはデフォルトルートなるルーティングがあります。
これは他のどのルートにもマッチしなかったときに呼ばれるいわゆるワイルドカード的な存在です。

REST API にはこのような仕組みは存在しません。
どのような挙動をするのか確認してみました。


outline

- ・構成
- ・Terraform
- ・検証

## 構成

API Gateway と Lambda を作ります

![arch.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/arch.png)

GET /hello とデフォルトルート ($default) の2つを用意し、
どちらのルートも Lambda へリクエストを向けます


AWSコンソールはこのような感じです。


![awsconsole.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/awsconsole.png)

## Terraform

### `.gitignore`

```gitignore
.terraform
*.zip
*.tfstate

```

### `apigateway_api_routes.tf`

```tf
# デフォルトルート
resource "aws_apigatewayv2_route" "default" {
  api_id    = aws_apigatewayv2_api.main.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

# GET /hello
resource "aws_apigatewayv2_route" "get_hello" {
  api_id    = aws_apigatewayv2_api.main.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

# Integration (Lambda)
resource "aws_apigatewayv2_integration" "lambda" {
  api_id                 = aws_apigatewayv2_api.main.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.main.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_lambda_permission" "apigateway" {
  statement_id  = "apigateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}

```

### `apigateway_api.tf`

```tf
# API Gateway
resource "aws_apigatewayv2_api" "main" {
  name          = var.identifier
  protocol_type = "HTTP"
}

# デフォルトステージ
resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.main.id
  name        = "$default"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.apigateway.arn
    format = jsonencode({
      requestId      = "$context.requestId"
      ip             = "$context.identity.sourceIp"
      requestTime    = "$context.requestTime"
      httpMethod     = "$context.httpMethod"
      routeKey       = "$context.routeKey"
      status         = "$context.status"
      responseLength = "$context.responseLength"
      integrationErrorMessage = "$context.integrationErrorMessage"
    })
  }
}

resource "aws_cloudwatch_log_group" "apigateway" {
  name              = "/aws/apigateway/${var.identifier}"
  retention_in_days = 14
}

```

### `lambda_app.py`

```py
def lambda_handler(event, context):
    print('routeKey:', event['routeKey'])
    print('rawPath:', event['rawPath'])
    print('queryStringParameters:', event.get('queryStringParameters', ''))
    print('body:', event.get('body', ''))
    print('requestContext.http.method:', event['requestContext']['http']['method'])
    print('requestContext.http.path:', event['requestContext']['http']['path'])

    return {
        "statusCode": 200,
        "body": "OK",
    }
```

### `lambda_role.tf`

```tf
resource "aws_iam_role" "lambda" {
  name = "${var.identifier}-lambda"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

```

### `lambda.tf`

```tf
resource "aws_lambda_function" "main" {
  function_name    = var.identifier

  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  handler          = "app.lambda_handler"

  role             = aws_iam_role.lambda.arn
  runtime          = "python3.13"
  architectures    = ["arm64"]
  memory_size      = 128
  timeout          = 10
}

data "archive_file" "lambda" {
  type        = "zip"
  output_path = "${path.module}/lambda.zip"

  source {
    filename = "app.py"
    content = file("${path.module}/lambda_app.py")
  }
}

resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${aws_lambda_function.main.function_name}"
  retention_in_days = 14
}

```

### `main.tf`

```tf
terraform {
  required_version = "1.15.8"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  # backend "s3" {
  #   bucket = ""
  #   key    = ""
  #   region = "ap-northeast-1"
  # }
}

provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = {
      terraform = "try-httpapi"
    }
  }
}

```

### `outputs.tf`

```tf

```

### `variables.tf`

```tf
variable "identifier" {
  type = string
  default = "try-httpapi"
}

```

### memos

API Gateway です。HTTP API を使います

mark: `apigateway_api.tf:2`

デフォルトルートです

mark: `apigateway_api_routes.tf:2`

Lambda です。デフォルトルートおよび GET /hello のバックエンドとなります

mark: `lambda.tf:1`

Lambda では event をログ出力します

mark: `lambda_app.py:2`

API Gateway のステージです。デフォルトステージにします

mark: `apigateway_api.tf:8`

- [https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-stages.html](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-stages.html)
**検証**

では、実際に確認していきましょう。
まずは GET /hello からです。


## GET /hello

```bash
curl 'https://{id}.execute-api.{region}.amazonaws.com/hello'

```

こちらは API Gateway へルートが定義されてますので、デフォルトルートは通りません。
Lambda へも "routeKey": "GET /hello" という値が渡ってきてます。


![./result-get-hello.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/./result-get-hello.png)

## GET /not-exists

```bash
curl 'https://{id}.execute-api.{region}.amazonaws.com/not-exists'

```

こちらは未定義のルートです。そのためデフォルトルートへ入ります。
Lambda へも "routeKey": "$default" という値が渡ってきました。


![./result-get-not-exists.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/./result-get-not-exists.png)

## POST /not-exists

```bash
curl -X POST --json '{"a":"aa"}' 'https://{id}.execute-api.{region}.amazonaws.com/not-exists'

```

POST でも同じくデフォルトルートへ入ります。


![./result-post-not-exists.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/./result-post-not-exists.png)

## DELETE /not-exists

```bash
curl -X DELETE 'https://{id}.execute-api.{region}.amazonaws.com/not-exists'

```

DELETE でも同じくデフォルトルートへ入ります。


![./result-delete-not-exists.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/./result-delete-not-exists.png)

## QUERY /not-exists

```bash
curl -X QUERY --json '{"a":"aa"}' 'https://{id}.execute-api.{region}.amazonaws.com/not-exists'

```

最近標準化された HTTP QUERY メソッドでも同じくデフォルトルートへ渡りました。
ほかの HTTP メソッドでも試しましたが、マッチするルートがなければとにかくデフォルトルートへ渡るようです。


![./result-query-not-exists.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/./result-query-not-exists.png)

## POST /hello

```bash
curl -X POST --json '{"a":"aa"}' 'https://{id}.execute-api.{region}.amazonaws.com/hello' 

```

いちおう POST /hello も呼び出しました。
これも未定義ですのでデフォルトルートに入ります。


![./result-post-hello.png](https://lab.enuesaa.dev/prototype/aws-api-gateway-http-api-default-route-lambda/./result-post-hello.png)

## Links

- [https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-develop-routes.html#http-api-develop-routes.default](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-develop-routes.html#http-api-develop-routes.default)
## おわり

デフォルトルートは便利な機能ですが、実際どういうユースケースがあるのだろうと思い挙動を検証してみました。


