Serverless Express + Express v5 で AWS サーバーレス REST API を構築してみた(AWS CDK)

Serverless Express + Express v5 で AWS サーバーレス REST API を構築してみた(AWS CDK)

Clock Icon2025.06.06

こんにちは、製造ビジネステクノロジー部の若槻です。

Node.js の Web フレームワークである Express の新しいバージョンである「Express v5」が昨年の10月にリリースされました。v5 では古い Node.js バージョンのサポート停止やセキュリティの改善が行われ、また新しい機能もいくつか追加されています。

https://github.com/expressjs/express/releases/tag/v5.0.0

そして昨年同月には Express をサーバーレス環境で実装できる Serverless Express でも Express v5 に対応したバージョンがリリースされました。

https://github.com/CodeGenieApp/serverless-express/releases/tag/v4.16.0

今回は、Serverless Express + Express v5 を AWS サーバーレス構成(Amazon API Gateway Lambda プロキシ統合)で利用して実装した REST API を AWS CDK で構築してみました。

試してみた

Express v5 と Serverless Express v4.16.0 を使って、AWS Lambda 上で動作する REST API を構築します。

package.json
{
  "dependencies": {
    "@codegenie/serverless-express": "4.16.0",
    "express": "5.1.0"
  }
}

Lambda 関数のハンドラーコードです。バージョンアップ移行を想定して v4 と v5 で共通して利用できるコードとなっています。また Request と Response の内容をログに出力して大きな違いがあるか確認できるようにしています。

src/rest-api-router.ts
import serverlessExpress from "@codegenie/serverless-express";
import cors from "cors";
import express, { Request, Response } from "express";

const app = express();
app.use(cors());
app.use(express.json());

app.get("/hello", (req: Request, res: Response): void => {
  console.log(req);
  console.log(res);

  res.status(200).send({ message: "Hello, world!" });
});

app.use((_: Request, res: Response): void => {
  res.status(404).send({ error: "Not Found!" });
});

export const handler = serverlessExpress({ app });

Amazon API Gateway Lambda プロキシ統合の AWS CDK スタックコードです。

lib/main-stack.ts
import * as cdk from "aws-cdk-lib";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as logs from "aws-cdk-lib/aws-logs";
import * as lambda_nodejs from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    /**
     * Lambda Function
     */
    const handler = new lambda_nodejs.NodejsFunction(this, "Handler", {
      entry: "src/rest-api-router.ts",
      logGroup: new logs.LogGroup(this, "LogGroup"),
    });

    /**
     * API Gateway REST API
     */
    new apigateway.LambdaRestApi(this, "RestApi", {
      handler,
    });
  }
}

上記をデプロイして、API Gateway のエンドポイントを取得したら、curl コマンドで動作確認をしてみます。

/hello エンドポイントに GET リクエストを送信すると、期待通りのレスポンスが返ってきました。良さそうですね。

$ curl -i https://2efkej2n93.execute-api.ap-northeast-1.amazonaws.com/prod/hello
HTTP/2 200
content-type: application/json; charset=utf-8
content-length: 27
date: Thu, 05 Jun 2025 15:00:23 GMT
x-amzn-trace-id: Root=1-6841b106-5664be012880830c31638ace;Parent=15c89c48b7a7e71d;Sampled=0;Lineage=1:a5c443a8:0
x-amzn-requestid: 6d1e3f94-12f5-42f8-9695-d334d40d3b07
access-control-allow-origin: *
x-amzn-remapped-content-length: 27
x-amz-apigw-id: LsiZJHucNjMECHA=
etag: W/"1b-tDnArHZ232y12bSoTiMc+/cz4as"
x-powered-by: Express
x-cache: Miss from cloudfront
via: 1.1 32dd1d6226b63466b55c722fe4ab7742.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P3
x-amz-cf-id: F6wS1ktc_0k9BVhGE1FLzkiMpFIsmz43toVInrZd4EnH9AC4I2rwAg==

{"message":"Hello, world!"}

CloudWatch Logs で Lambda 関数のログを確認すると、リクエストとレスポンスの詳細が出力されていることがわかります。リクエストのパラメーターやヘッダー、レスポンスのステータスコードやヘッダーなどが確認できます。

Request
2025-06-05T15:00:22.989Z	4ba5c9c5-6c0e-49b0-bb8c-7b935d023600	INFO	<ref *2> IncomingMessage {
  _readableState: ReadableState {
    objectMode: false,
    highWaterMark: 16384,
    buffer: BufferList { head: null, tail: null, length: 0 },
    length: 0,
    pipes: [],
    flowing: null,
    ended: false,
    endEmitted: false,
    reading: false,
    constructed: true,
    sync: true,
    needReadable: false,
    emittedReadable: false,
    readableListening: false,
    resumeScheduled: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: true,
    destroyed: false,
    errored: null,
    closed: false,
    closeEmitted: false,
    defaultEncoding: 'utf8',
    awaitDrainWriters: null,
    multiAwaitDrain: false,
    readingMore: true,
    dataEmitted: false,
    decoder: null,
    encoding: null,
    [Symbol(kPaused)]: null
  },
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  socket: {
    encrypted: true,
    readable: true,
    remoteAddress: '104.28.243.105',
    address: [Function: address],
    end: {},
    destroy: {}
  },
  httpVersionMajor: '1',
  httpVersionMinor: '1',
  httpVersion: '1.1',
  complete: true,
  rawHeaders: [
    'accept',
    '*/*',
    'cloudfront-forwarded-proto',
    'https',
    'cloudfront-is-desktop-viewer',
    'true',
    'cloudfront-is-mobile-viewer',
    'false',
    'cloudfront-is-smarttv-viewer',
    'false',
    'cloudfront-is-tablet-viewer',
    'false',
    'cloudfront-viewer-asn',
    '13335',
    'cloudfront-viewer-country',
    'JP',
    'host',
    '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
    'user-agent',
    'curl/8.7.1',
    'via',
    '2.0 32dd1d6226b63466b55c722fe4ab7742.cloudfront.net (CloudFront)',
    'x-amz-cf-id',
    'F6wS1ktc_0k9BVhGE1FLzkiMpFIsmz43toVInrZd4EnH9AC4I2rwAg==',
    'x-amzn-trace-id',
    'Root=1-6841b106-5664be012880830c31638ace',
    'x-forwarded-for',
    '104.28.243.105, 18.68.35.142',
    'x-forwarded-port',
    '443',
    'x-forwarded-proto',
    'https'
  ],
  rawTrailers: [],
  aborted: false,
  upgrade: null,
  url: '/hello',
  method: 'GET',
  statusCode: null,
  statusMessage: null,
  client: {
    encrypted: true,
    readable: true,
    remoteAddress: '104.28.243.105',
    address: [Function: address],
    end: {},
    destroy: {}
  },
  _consuming: false,
  _dumped: false,
  ip: '104.28.243.105',
  body: undefined,
  _read: [Function (anonymous)],
  res: <ref *1> ServerResponse {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: false,
    sendDate: true,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: false,
    _headerSent: false,
    _closed: false,
    socket: {
      _writableState: {},
      writable: true,
      on: {},
      removeListener: {},
      destroy: {},
      cork: {},
      uncork: {},
      write: [Function: write],
      _httpMessage: [Circular *1]
    },
    _header: '',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    req: [Circular *2],
    _sent100: false,
    _expect_continue: false,
    locals: [Object: null prototype] {},
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kEndCalled)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      'x-powered-by': [Array],
      'access-control-allow-origin': [Array]
    },
    [Symbol(Response body)]: [],
    [Symbol(Response headers)]: {}
  },
  next: [Function: next],
  baseUrl: '',
  originalUrl: '/hello',
  _parsedUrl: Url {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: null,
    query: null,
    pathname: '/hello',
    path: '/hello',
    href: '/hello',
    _raw: '/hello'
  },
  params: [Object: null prototype] {},
  route: Route {
    path: '/hello',
    stack: [ [Layer] ],
    methods: [Object: null prototype] { get: true }
  },
  [Symbol(kCapture)]: false,
  [Symbol(kHeaders)]: {
    accept: '*/*',
    'cloudfront-forwarded-proto': 'https',
    'cloudfront-is-desktop-viewer': 'true',
    'cloudfront-is-mobile-viewer': 'false',
    'cloudfront-is-smarttv-viewer': 'false',
    'cloudfront-is-tablet-viewer': 'false',
    'cloudfront-viewer-asn': '13335',
    'cloudfront-viewer-country': 'JP',
    host: '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
    'user-agent': 'curl/8.7.1',
    via: '2.0 32dd1d6226b63466b55c722fe4ab7742.cloudfront.net (CloudFront)',
    'x-amz-cf-id': 'F6wS1ktc_0k9BVhGE1FLzkiMpFIsmz43toVInrZd4EnH9AC4I2rwAg==',
    'x-amzn-trace-id': 'Root=1-6841b106-5664be012880830c31638ace',
    'x-forwarded-for': '104.28.243.105, 18.68.35.142',
    'x-forwarded-port': '443',
    'x-forwarded-proto': 'https'
  },
  [Symbol(kHeadersCount)]: 32,
  [Symbol(kTrailers)]: null,
  [Symbol(kTrailersCount)]: 0
}
Response
2025-06-05T15:00:23.023Z	4ba5c9c5-6c0e-49b0-bb8c-7b935d023600	INFO	<ref *1> ServerResponse {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  outputData: [],
  outputSize: 0,
  writable: true,
  destroyed: false,
  _last: false,
  chunkedEncoding: false,
  shouldKeepAlive: true,
  maxRequestsOnConnectionReached: false,
  _defaultKeepAlive: true,
  useChunkedEncodingByDefault: false,
  sendDate: true,
  _removedConnection: false,
  _removedContLen: false,
  _removedTE: false,
  strictContentLength: false,
  _contentLength: null,
  _hasBody: true,
  _trailer: '',
  finished: false,
  _headerSent: false,
  _closed: false,
  socket: {
    _writableState: {},
    writable: true,
    on: {},
    removeListener: {},
    destroy: {},
    cork: {},
    uncork: {},
    write: [Function: write],
    _httpMessage: [Circular *1]
  },
  _header: '',
  _keepAliveTimeout: 0,
  _onPendingData: [Function: nop],
  req: IncomingMessage {
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: null,
      ended: false,
      endEmitted: false,
      reading: false,
      constructed: true,
      sync: true,
      needReadable: false,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      errorEmitted: false,
      emitClose: true,
      autoDestroy: true,
      destroyed: false,
      errored: null,
      closed: false,
      closeEmitted: false,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: true,
      dataEmitted: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: null
    },
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    socket: {
      encrypted: true,
      readable: true,
      remoteAddress: '104.28.243.105',
      address: [Function: address],
      end: {},
      destroy: {}
    },
    httpVersionMajor: '1',
    httpVersionMinor: '1',
    httpVersion: '1.1',
    complete: true,
    rawHeaders: [
      'accept',
      '*/*',
      'cloudfront-forwarded-proto',
      'https',
      'cloudfront-is-desktop-viewer',
      'true',
      'cloudfront-is-mobile-viewer',
      'false',
      'cloudfront-is-smarttv-viewer',
      'false',
      'cloudfront-is-tablet-viewer',
      'false',
      'cloudfront-viewer-asn',
      '13335',
      'cloudfront-viewer-country',
      'JP',
      'host',
      '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
      'user-agent',
      'curl/8.7.1',
      'via',
      '2.0 32dd1d6226b63466b55c722fe4ab7742.cloudfront.net (CloudFront)',
      'x-amz-cf-id',
      'F6wS1ktc_0k9BVhGE1FLzkiMpFIsmz43toVInrZd4EnH9AC4I2rwAg==',
      'x-amzn-trace-id',
      'Root=1-6841b106-5664be012880830c31638ace',
      'x-forwarded-for',
      '104.28.243.105, 18.68.35.142',
      'x-forwarded-port',
      '443',
      'x-forwarded-proto',
      'https'
    ],
    rawTrailers: [],
    aborted: false,
    upgrade: null,
    url: '/hello',
    method: 'GET',
    statusCode: null,
    statusMessage: null,
    client: {
      encrypted: true,
      readable: true,
      remoteAddress: '104.28.243.105',
      address: [Function: address],
      end: {},
      destroy: {}
    },
    _consuming: false,
    _dumped: false,
    ip: '104.28.243.105',
    body: undefined,
    _read: [Function (anonymous)],
    res: [Circular *1],
    next: [Function: next],
    baseUrl: '',
    originalUrl: '/hello',
    _parsedUrl: Url {
      protocol: null,
      slashes: null,
      auth: null,
      host: null,
      port: null,
      hostname: null,
      hash: null,
      search: null,
      query: null,
      pathname: '/hello',
      path: '/hello',
      href: '/hello',
      _raw: '/hello'
    },
    params: [Object: null prototype] {},
    route: Route {
      path: '/hello',
      stack: [Array],
      methods: [Object: null prototype]
    },
    [Symbol(kCapture)]: false,
    [Symbol(kHeaders)]: {
      accept: '*/*',
      'cloudfront-forwarded-proto': 'https',
      'cloudfront-is-desktop-viewer': 'true',
      'cloudfront-is-mobile-viewer': 'false',
      'cloudfront-is-smarttv-viewer': 'false',
      'cloudfront-is-tablet-viewer': 'false',
      'cloudfront-viewer-asn': '13335',
      'cloudfront-viewer-country': 'JP',
      host: '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
      'user-agent': 'curl/8.7.1',
      via: '2.0 32dd1d6226b63466b55c722fe4ab7742.cloudfront.net (CloudFront)',
      'x-amz-cf-id': 'F6wS1ktc_0k9BVhGE1FLzkiMpFIsmz43toVInrZd4EnH9AC4I2rwAg==',
      'x-amzn-trace-id': 'Root=1-6841b106-5664be012880830c31638ace',
      'x-forwarded-for': '104.28.243.105, 18.68.35.142',
      'x-forwarded-port': '443',
      'x-forwarded-proto': 'https'
    },
    [Symbol(kHeadersCount)]: 32,
    [Symbol(kTrailers)]: null,
    [Symbol(kTrailersCount)]: 0
  },
  _sent100: false,
  _expect_continue: false,
  locals: [Object: null prototype] {},
  [Symbol(kCapture)]: false,
  [Symbol(kBytesWritten)]: 0,
  [Symbol(kEndCalled)]: false,
  [Symbol(kNeedDrain)]: false,
  [Symbol(corked)]: 0,
  [Symbol(kOutHeaders)]: [Object: null prototype] {
    'x-powered-by': [ 'X-Powered-By', 'Express' ],
    'access-control-allow-origin': [ 'Access-Control-Allow-Origin', '*' ]
  },
  [Symbol(Response body)]: [],
  [Symbol(Response headers)]: {}
}

存在しないエンドポイントにリクエストを送信すると、404 Not Found エラーが返ってきます。こちらも期待通りの動作です。

$ curl -i https://2efkej2n93.execute-api.ap-northeast-1.amazonaws.com/prod/
HTTP/2 404
content-type: application/json; charset=utf-8
content-length: 22
date: Thu, 05 Jun 2025 15:00:16 GMT
x-amzn-trace-id: Root=1-6841b100-22de20f149607b8e585ec987;Parent=5b984b6a319ca44c;Sampled=0;Lineage=1:a5c443a8:0
x-amzn-requestid: c41658c2-3ac2-4220-bf3b-a86b25c8ec46
access-control-allow-origin: *
x-amzn-remapped-content-length: 22
x-amz-apigw-id: LsiYFGKLNjMEHVg=
etag: W/"16-zbb+ntH1VuqiqzB6oMfB/xQSB0o"
x-powered-by: Express
x-cache: Error from cloudfront
via: 1.1 32dd1d6226b63466b55c722fe4ab7742.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P3
x-amz-cf-id: SsTWn_E5CKPfcK0NQkk-9eRhKljlSeFw09CFOpKPYBZqCSUQuFF4Jw==

{"error":"Not Found!"}

比較:Express v4 の場合

比較として同じハンドラーおよび AWS CDK コードで Express v4 を使った場合の挙動も確認してみました。

package.json
{
  "dependencies": {
    "@codegenie/serverless-express": "4.16.0",
    "express": "4.21.2"
  }
}

結論としては Express v5 と同様に動作し、リクエストとレスポンスの内容も同じように出力されました。今回試した範囲だと破壊的変更には該当しなかったようです。動作確認結果は v5 と同様で長いので折りたたんでいます。

Express v4 動作確認結果
$ curl -i https://2efkej2n93.execute-api.ap-northeast-1.amazonaws.com/prod/hello
HTTP/2 200
content-type: application/json; charset=utf-8
content-length: 27
date: Thu, 05 Jun 2025 14:43:41 GMT
x-amzn-trace-id: Root=1-6841ad1d-6cbdb9de5ff0e14c5e9be6d1;Parent=0cde1a5d4bd36356;Sampled=0;Lineage=1:a5c443a8:0
x-amzn-requestid: d72cb48c-cba3-4c80-946a-a2dad7101b01
access-control-allow-origin: *
x-amzn-remapped-content-length: 27
x-amz-apigw-id: Lsf8nEHntjMEHLA=
etag: W/"1b-tDnArHZ232y12bSoTiMc+/cz4as"
x-powered-by: Express
x-cache: Miss from cloudfront
via: 1.1 0b1c9648687ba0cb353e184231f063b2.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P3
x-amz-cf-id: AL0cRnh9JJPK_1_YX_FBufxzLQkohd3sPG-H8A83jndWoUDRbxEifQ==

{"message":"Hello, world!"}
Request
2025-06-05T14:43:41.666Z	5d1c14f1-796b-4e63-aeef-67ce92547475	INFO	<ref *2> IncomingMessage {
  _readableState: ReadableState {
    objectMode: false,
    highWaterMark: 16384,
    buffer: BufferList { head: null, tail: null, length: 0 },
    length: 0,
    pipes: [],
    flowing: null,
    ended: false,
    endEmitted: false,
    reading: false,
    constructed: true,
    sync: true,
    needReadable: false,
    emittedReadable: false,
    readableListening: false,
    resumeScheduled: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: true,
    destroyed: false,
    errored: null,
    closed: false,
    closeEmitted: false,
    defaultEncoding: 'utf8',
    awaitDrainWriters: null,
    multiAwaitDrain: false,
    readingMore: true,
    dataEmitted: false,
    decoder: null,
    encoding: null,
    [Symbol(kPaused)]: null
  },
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  socket: {
    encrypted: true,
    readable: true,
    remoteAddress: '104.28.243.105',
    address: [Function: address],
    end: {},
    destroy: {}
  },
  httpVersionMajor: '1',
  httpVersionMinor: '1',
  httpVersion: '1.1',
  complete: true,
  rawHeaders: [
    'accept',
    '*/*',
    'cloudfront-forwarded-proto',
    'https',
    'cloudfront-is-desktop-viewer',
    'true',
    'cloudfront-is-mobile-viewer',
    'false',
    'cloudfront-is-smarttv-viewer',
    'false',
    'cloudfront-is-tablet-viewer',
    'false',
    'cloudfront-viewer-asn',
    '13335',
    'cloudfront-viewer-country',
    'JP',
    'host',
    '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
    'user-agent',
    'curl/8.7.1',
    'via',
    '2.0 0b1c9648687ba0cb353e184231f063b2.cloudfront.net (CloudFront)',
    'x-amz-cf-id',
    'AL0cRnh9JJPK_1_YX_FBufxzLQkohd3sPG-H8A83jndWoUDRbxEifQ==',
    'x-amzn-trace-id',
    'Root=1-6841ad1d-6cbdb9de5ff0e14c5e9be6d1',
    'x-forwarded-for',
    '104.28.243.105, 18.68.35.146',
    'x-forwarded-port',
    '443',
    'x-forwarded-proto',
    'https'
  ],
  rawTrailers: [],
  aborted: false,
  upgrade: null,
  url: '/hello',
  method: 'GET',
  statusCode: null,
  statusMessage: null,
  client: {
    encrypted: true,
    readable: true,
    remoteAddress: '104.28.243.105',
    address: [Function: address],
    end: {},
    destroy: {}
  },
  _consuming: false,
  _dumped: false,
  ip: '104.28.243.105',
  body: {},
  _read: [Function (anonymous)],
  next: [Function: next],
  baseUrl: '',
  originalUrl: '/hello',
  _parsedUrl: Url {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: null,
    query: null,
    pathname: '/hello',
    path: '/hello',
    href: '/hello',
    _raw: '/hello'
  },
  params: {},
  query: {},
  res: <ref *1> ServerResponse {
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    outputData: [],
    outputSize: 0,
    writable: true,
    destroyed: false,
    _last: false,
    chunkedEncoding: false,
    shouldKeepAlive: true,
    maxRequestsOnConnectionReached: false,
    _defaultKeepAlive: true,
    useChunkedEncodingByDefault: false,
    sendDate: true,
    _removedConnection: false,
    _removedContLen: false,
    _removedTE: false,
    strictContentLength: false,
    _contentLength: null,
    _hasBody: true,
    _trailer: '',
    finished: false,
    _headerSent: false,
    _closed: false,
    socket: {
      _writableState: {},
      writable: true,
      on: {},
      removeListener: {},
      destroy: {},
      cork: {},
      uncork: {},
      write: [Function: write],
      _httpMessage: [Circular *1]
    },
    _header: '',
    _keepAliveTimeout: 0,
    _onPendingData: [Function: nop],
    req: [Circular *2],
    _sent100: false,
    _expect_continue: false,
    locals: [Object: null prototype] {},
    [Symbol(kCapture)]: false,
    [Symbol(kBytesWritten)]: 0,
    [Symbol(kEndCalled)]: false,
    [Symbol(kNeedDrain)]: false,
    [Symbol(corked)]: 0,
    [Symbol(kOutHeaders)]: [Object: null prototype] {
      'x-powered-by': [Array],
      'access-control-allow-origin': [Array]
    },
    [Symbol(Response body)]: [],
    [Symbol(Response headers)]: {}
  },
  route: Route { path: '/hello', stack: [ [Layer] ], methods: { get: true } },
  [Symbol(kCapture)]: false,
  [Symbol(kHeaders)]: {
    accept: '*/*',
    'cloudfront-forwarded-proto': 'https',
    'cloudfront-is-desktop-viewer': 'true',
    'cloudfront-is-mobile-viewer': 'false',
    'cloudfront-is-smarttv-viewer': 'false',
    'cloudfront-is-tablet-viewer': 'false',
    'cloudfront-viewer-asn': '13335',
    'cloudfront-viewer-country': 'JP',
    host: '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
    'user-agent': 'curl/8.7.1',
    via: '2.0 0b1c9648687ba0cb353e184231f063b2.cloudfront.net (CloudFront)',
    'x-amz-cf-id': 'AL0cRnh9JJPK_1_YX_FBufxzLQkohd3sPG-H8A83jndWoUDRbxEifQ==',
    'x-amzn-trace-id': 'Root=1-6841ad1d-6cbdb9de5ff0e14c5e9be6d1',
    'x-forwarded-for': '104.28.243.105, 18.68.35.146',
    'x-forwarded-port': '443',
    'x-forwarded-proto': 'https'
  },
  [Symbol(kHeadersCount)]: 32,
  [Symbol(kTrailers)]: null,
  [Symbol(kTrailersCount)]: 0
}
Response
2025-06-05T14:43:41.728Z	5d1c14f1-796b-4e63-aeef-67ce92547475	INFO	<ref *1> ServerResponse {
  _events: [Object: null prototype] {},
  _eventsCount: 0,
  _maxListeners: undefined,
  outputData: [],
  outputSize: 0,
  writable: true,
  destroyed: false,
  _last: false,
  chunkedEncoding: false,
  shouldKeepAlive: true,
  maxRequestsOnConnectionReached: false,
  _defaultKeepAlive: true,
  useChunkedEncodingByDefault: false,
  sendDate: true,
  _removedConnection: false,
  _removedContLen: false,
  _removedTE: false,
  strictContentLength: false,
  _contentLength: null,
  _hasBody: true,
  _trailer: '',
  finished: false,
  _headerSent: false,
  _closed: false,
  socket: {
    _writableState: {},
    writable: true,
    on: {},
    removeListener: {},
    destroy: {},
    cork: {},
    uncork: {},
    write: [Function: write],
    _httpMessage: [Circular *1]
  },
  _header: '',
  _keepAliveTimeout: 0,
  _onPendingData: [Function: nop],
  req: IncomingMessage {
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: null,
      ended: false,
      endEmitted: false,
      reading: false,
      constructed: true,
      sync: true,
      needReadable: false,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      errorEmitted: false,
      emitClose: true,
      autoDestroy: true,
      destroyed: false,
      errored: null,
      closed: false,
      closeEmitted: false,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: true,
      dataEmitted: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: null
    },
    _events: [Object: null prototype] {},
    _eventsCount: 0,
    _maxListeners: undefined,
    socket: {
      encrypted: true,
      readable: true,
      remoteAddress: '104.28.243.105',
      address: [Function: address],
      end: {},
      destroy: {}
    },
    httpVersionMajor: '1',
    httpVersionMinor: '1',
    httpVersion: '1.1',
    complete: true,
    rawHeaders: [
      'accept',
      '*/*',
      'cloudfront-forwarded-proto',
      'https',
      'cloudfront-is-desktop-viewer',
      'true',
      'cloudfront-is-mobile-viewer',
      'false',
      'cloudfront-is-smarttv-viewer',
      'false',
      'cloudfront-is-tablet-viewer',
      'false',
      'cloudfront-viewer-asn',
      '13335',
      'cloudfront-viewer-country',
      'JP',
      'host',
      '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
      'user-agent',
      'curl/8.7.1',
      'via',
      '2.0 0b1c9648687ba0cb353e184231f063b2.cloudfront.net (CloudFront)',
      'x-amz-cf-id',
      'AL0cRnh9JJPK_1_YX_FBufxzLQkohd3sPG-H8A83jndWoUDRbxEifQ==',
      'x-amzn-trace-id',
      'Root=1-6841ad1d-6cbdb9de5ff0e14c5e9be6d1',
      'x-forwarded-for',
      '104.28.243.105, 18.68.35.146',
      'x-forwarded-port',
      '443',
      'x-forwarded-proto',
      'https'
    ],
    rawTrailers: [],
    aborted: false,
    upgrade: null,
    url: '/hello',
    method: 'GET',
    statusCode: null,
    statusMessage: null,
    client: {
      encrypted: true,
      readable: true,
      remoteAddress: '104.28.243.105',
      address: [Function: address],
      end: {},
      destroy: {}
    },
    _consuming: false,
    _dumped: false,
    ip: '104.28.243.105',
    body: {},
    _read: [Function (anonymous)],
    next: [Function: next],
    baseUrl: '',
    originalUrl: '/hello',
    _parsedUrl: Url {
      protocol: null,
      slashes: null,
      auth: null,
      host: null,
      port: null,
      hostname: null,
      hash: null,
      search: null,
      query: null,
      pathname: '/hello',
      path: '/hello',
      href: '/hello',
      _raw: '/hello'
    },
    params: {},
    query: {},
    res: [Circular *1],
    route: Route { path: '/hello', stack: [Array], methods: [Object] },
    [Symbol(kCapture)]: false,
    [Symbol(kHeaders)]: {
      accept: '*/*',
      'cloudfront-forwarded-proto': 'https',
      'cloudfront-is-desktop-viewer': 'true',
      'cloudfront-is-mobile-viewer': 'false',
      'cloudfront-is-smarttv-viewer': 'false',
      'cloudfront-is-tablet-viewer': 'false',
      'cloudfront-viewer-asn': '13335',
      'cloudfront-viewer-country': 'JP',
      host: '2efkej2n93.execute-api.ap-northeast-1.amazonaws.com',
      'user-agent': 'curl/8.7.1',
      via: '2.0 0b1c9648687ba0cb353e184231f063b2.cloudfront.net (CloudFront)',
      'x-amz-cf-id': 'AL0cRnh9JJPK_1_YX_FBufxzLQkohd3sPG-H8A83jndWoUDRbxEifQ==',
      'x-amzn-trace-id': 'Root=1-6841ad1d-6cbdb9de5ff0e14c5e9be6d1',
      'x-forwarded-for': '104.28.243.105, 18.68.35.146',
      'x-forwarded-port': '443',
      'x-forwarded-proto': 'https'
    },
    [Symbol(kHeadersCount)]: 32,
    [Symbol(kTrailers)]: null,
    [Symbol(kTrailersCount)]: 0
  },
  _sent100: false,
  _expect_continue: false,
  locals: [Object: null prototype] {},
  [Symbol(kCapture)]: false,
  [Symbol(kBytesWritten)]: 0,
  [Symbol(kEndCalled)]: false,
  [Symbol(kNeedDrain)]: false,
  [Symbol(corked)]: 0,
  [Symbol(kOutHeaders)]: [Object: null prototype] {
    'x-powered-by': [ 'X-Powered-By', 'Express' ],
    'access-control-allow-origin': [ 'Access-Control-Allow-Origin', '*' ]
  },
  [Symbol(Response body)]: [],
  [Symbol(Response headers)]: {}
}
$ curl -i https://2efkej2n93.execute-api.ap-northeast-1.amazonaws.com/prod/hoge
HTTP/2 404
content-type: application/json; charset=utf-8
content-length: 22
date: Thu, 05 Jun 2025 14:46:32 GMT
x-amzn-trace-id: Root=1-6841adc8-15b5ca6b6e93c8ed582c1d4a;Parent=3e39d702fcf5d4c9;Sampled=0;Lineage=1:a5c443a8:0
x-amzn-requestid: 047853c4-a91f-4639-b6d1-53146dec4041
access-control-allow-origin: *
x-amzn-remapped-content-length: 22
x-amz-apigw-id: LsgXUHKDtjMECqg=
etag: W/"16-zbb+ntH1VuqiqzB6oMfB/xQSB0o"
x-powered-by: Express
x-cache: Error from cloudfront
via: 1.1 5ad6ede360a0a258285ddf7aca196f2a.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT20-P3
x-amz-cf-id: hu5b1VE3BlfSK2JlbXyIU6LwntSR_F23WP_H-tIN7VE7XCSdUqF7Bg==

{"error":"Not Found!"}

おわりに

Serverless Express + Express v5 を AWS サーバーレス構成(Amazon API Gateway Lambda プロキシ統合)で利用して実装した REST API を AWS CDK で構築してみました。

今回試した限りだと、v4 で利用していたハンドラーのコードを特に修正することなく v5 へ移行できることが確認できました。

ただし下記のドキュメントにある通り破壊的変更もいくつかあります。例えば v4 まで使えていた app.del() が削除されて app.delete() に統合されるなど、v5 ではいくつかの API が変更や削除されています。移行を検討される方は、事前に破壊的変更の内容を確認しておくことをおすすめします。

https://expressjs.com/ja/guide/migrating-5.html

以上

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.