API Gateway’s WebSocket routing isn’t just about sending messages to the right backend; it’s about making sure the connection itself is routed intelligently based on the message content.

Let’s see it in action. Imagine a chat application where messages are routed to different backend services based on the chat room ID.

{
  "action": "sendToRoom",
  "roomId": "general",
  "message": "Hello everyone!"
}

This JSON payload needs to go to a service handling the "general" chat room. Here’s how we’d configure API Gateway to do that.

The core of this is the route selection expression. This is a string that API Gateway evaluates against the incoming message payload to determine which integration to invoke. For WebSocket APIs, this expression typically targets a field within the JSON payload.

In our chat example, we want to route based on the roomId. So, our route selection expression would be $request.body.roomId. When a message arrives, API Gateway parses the JSON, looks for the roomId field, and uses its value to match against predefined routes.

Here’s a simplified look at how you’d define routes in API Gateway for this scenario:

{
  "routes": [
    {
      "routeKey": "sendToRoom",
      "eventType": "MESSAGE",
      "target": "wss://chat-service-general.example.com/ws",
      "routeSelectionExpression": "$request.body.roomId"
    },
    {
      "routeKey": "joinRoom",
      "eventType": "MESSAGE",
      "target": "wss://user-management.example.com/ws",
      "routeSelectionExpression": "$request.body.action"
    }
    // ... other routes for different actions or room IDs
  ]
}

In this configuration:

  • routeKey: This is a static identifier for a route. For MESSAGE events, routeKey is often used to match against a specific field if routeSelectionExpression isn’t defined or if you want a primary match.
  • eventType: Specifies the type of WebSocket event. MESSAGE is for incoming messages.
  • target: The WebSocket URL of the backend integration.
  • routeSelectionExpression: This is where the magic happens. For a MESSAGE event, $request.body.roomId tells API Gateway to extract the value of the roomId field from the incoming JSON message.

When a message like {"action": "sendToRoom", "roomId": "general", "message": "Hello everyone!"} arrives, API Gateway evaluates $request.body.roomId. It sees "general". Then, it looks for a route where the routeSelectionExpression’s resolved value ("general") matches its own value. If we had a route with routeKey: "general" and routeSelectionExpression: "$request.body.roomId", this message would be sent to that route’s target.

However, the more common and flexible pattern is to use the routeSelectionExpression to directly determine the target, often by mapping specific values to specific integrations. So, if the expression resolves to "general", API Gateway looks for a route whose resolved routeSelectionExpression value is "general".

Let’s refine the routes to be more explicit about matching the resolved value of the routeSelectionExpression:

{
  "routes": [
    {
      "routeKey": "$request.body.roomId", // The route key itself is the expression
      "eventTypes": ["MESSAGE"],
      "target": "wss://chat-service-general.example.com/ws",
      "routeSelectionExpression": "$request.body.roomId"
    },
    {
      "routeKey": "$request.body.roomId", // Another route, but with a different target
      "eventTypes": ["MESSAGE"],
      "target": "wss://chat-service-dev.example.com/ws",
      "routeSelectionExpression": "$request.body.roomId"
    }
    // ... and so on for other room IDs
  ]
}

This is slightly misleading. The routeKey is not dynamically evaluated in the same way as routeSelectionExpression for MESSAGE events. For MESSAGE events, the routeSelectionExpression is always evaluated, and its result is used to select the route. The routeKey is more for static routes or other event types.

The correct way to configure dynamic routing based on message content for MESSAGE events is to have a single routeSelectionExpression that resolves to a value, and then have multiple routes where each route’s own routeKey matches one of the possible resolved values from the routeSelectionExpression.

Let’s correct the configuration for clarity and correctness:

{
  "routes": [
    {
      "routeKey": "general", // This route is for messages where roomId is 'general'
      "eventTypes": ["MESSAGE"],
      "target": "wss://chat-service-general.example.com/ws",
      "routeSelectionExpression": "$request.body.roomId"
    },
    {
      "routeKey": "development", // This route is for messages where roomId is 'development'
      "eventTypes": ["MESSAGE"],
      "target": "wss://chat-service-dev.example.com/ws",
      "routeSelectionExpression": "$request.body.roomId"
    },
    {
      "routeKey": "random", // And so on...
      "eventTypes": ["MESSAGE"],
      "target": "wss://chat-service-random.example.com/ws",
      "routeSelectionExpression": "$request.body.roomId"
    },
    {
      "routeKey": "$default", // Fallback route if no specific match
      "eventTypes": ["MESSAGE"],
      "target": "wss://default-handler.example.com/ws"
    }
  ]
}

Here, the routeSelectionExpression is "$request.body.roomId". When an incoming message is processed, API Gateway evaluates this expression. If the message is {"action": "sendToRoom", "roomId": "general", ...}, the expression evaluates to "general". API Gateway then looks for a route whose routeKey is "general". It finds the first route defined above and sends the message to wss://chat-service-general.example.com/ws. If the roomId was "development", it would match the second route, and so on. The $default route acts as a catch-all.

The most surprising true thing about API Gateway WebSocket routing is that the routeSelectionExpression for MESSAGE events always resolves to a value, and that value is then used to match against the routeKey of available routes. It’s not that the routeKey itself is dynamically evaluated to find the route; rather, the routeSelectionExpression dynamically produces a value that a static routeKey must match.

Consider this message: {"command": "createUser", "details": {"name": "Alice"}}. If your routeSelectionExpression is $request.body.command, API Gateway will resolve this to "createUser". It then searches for a route where the routeKey is literally "createUser".

The actual levers you control are the routeSelectionExpression string and the routeKey values for each defined route. The expression uses a simple JSONPath-like syntax (e.g., $request.body.fieldName, $request.body.nested.field). The routeKey is a string literal. You can also use $context.connectionId in expressions if you need to route based on the connection itself, though this is less common for message content-based routing.

The one thing most people don’t know is that the routeSelectionExpression can be arbitrarily complex and can include multiple fields, though it’s often best to keep it simple for maintainability. For instance, you could use $request.body.roomId + '-' + $request.body.action as an expression, expecting route keys like "general-sendToRoom". However, this quickly becomes unmanageable and is usually better handled by having separate routes for roomId and then potentially a secondary check or a different expression for action if needed.

The next concept you’ll run into is handling different eventTypes like CONNECT and DISCONNECT, which have their own specific routing behaviors and expressions.

Want structured learning?

Take the full Apigateway course →