Skip to main content
The AWS SDK for JavaScript v3 processes every operation call through an ordered pipeline of async functions called the middleware stack. Each function in the stack receives the request, can modify it, calls the next function, and can inspect or modify the response on the way back. This model replaces the event listener approach from v2, making it easier to reason about and debug what happens during a request’s lifecycle.

How the stack works

When you call client.send(command):
  1. Input parameters enter the top of the stack.
  2. Each middleware calls the next one in order, passing the (potentially modified) arguments.
  3. The HTTP handler sends the request to the service.
  4. The response travels back up through the same middleware in reverse order.
  5. The deserialized output is returned to your code.

Lifecycle steps

Every middleware is assigned to one of five lifecycle steps, executed in this order:
StepPurpose
initializeAdds default input values. The HTTP request has not been constructed yet.
serializeBuilds the HTTP request from input parameters. Validates input and creates the request object. Downstream middleware have access to args.request.
buildAugments the serialized request with stable headers such as Content-Length or a body checksum. Changes here apply to all retry attempts.
finalizeRequestPrepares the request for transmission: request signing, retry logic, hop-by-hop headers. The request is semantically complete at this stage.
deserializeConverts the raw HTTP response into a structured output object. The upstream middleware have access to result.output.

Adding middleware

Use client.middlewareStack.add() to insert a function into the stack. Middleware added to a client applies to all commands sent by that client.
client.middlewareStack.add(middlewareFn, options);
The middleware function signature is:
(next, context) => async (args) => {
  // inspect or modify args before the request
  const result = await next(args);
  // inspect or modify result after the response
  return result;
};
  • next: The next middleware in the stack. You must call it and return its result (unless you intentionally short-circuit).
  • context: An object containing clientName and commandName, plus any data shared across middleware.
  • args.input: The command’s input parameters.
  • args.request: The serialized HTTP request object (available from the build step onward).
  • result.output: The deserialized response (available from the deserialize step onward).

Options

name
string
A unique identifier for this middleware. Used when removing it or preventing duplicates with override.
step
string
required
The lifecycle step to attach to: "initialize", "serialize", "build", "finalizeRequest", or "deserialize".
priority
string
default:"normal"
Execution order within a step. "high" runs before normal middleware; "low" runs after.
override
boolean
default:"false"
When true, replaces any existing middleware with the same name instead of adding a second copy. Provide both name and override: true to avoid accidental duplication.
tags
string[]
Arbitrary labels for grouping or identifying middleware. Not used by the SDK itself.

Removing middleware

Remove middleware by the name you assigned when adding it:
client.middlewareStack.remove("MyMiddleware");

Examples

Logging requests and responses

This example from the SDK README logs the command context, input parameters, and output for every operation sent by the client:
const client = new DynamoDB({ region: "us-west-2" });

client.middlewareStack.add(
  (next, context) => async (args) => {
    console.log("AWS SDK context", context.clientName, context.commandName);
    console.log("AWS SDK request input", args.input);
    const result = await next(args);
    console.log("AWS SDK request output:", result.output);
    return result;
  },
  {
    name: "MyMiddleware",
    step: "build",
    override: true,
  }
);

await client.listTables({});

Injecting request headers

Add a custom header to every request. Because args.request is available at the build step and persists across retries, this is the right place for stable header injection:
const { S3 } = require("@aws-sdk/client-s3");
const client = new S3({ region: "us-west-2" });

client.middlewareStack.add(
  (next, context) => async (args) => {
    args.request.headers["x-amz-meta-foo"] = "bar";
    console.log("AWS SDK context", context.clientName, context.commandName);
    console.log("AWS SDK request input", args.input);
    const result = await next(args);
    console.log("AWS SDK request output:", result.output);
    return result;
  },
  {
    step: "build",
    name: "addFooMetadataMiddleware",
    tags: ["METADATA", "FOO"],
    override: true,
  }
);

await client.putObject(params);

Detailed request/response logging

For full request and response debugging, log args.request and result.response directly:
import { S3 } from "@aws-sdk/client-s3";

const client = new S3({ region: "us-west-2" });

client.middlewareStack.add(
  (next, context) => async (args) => {
    console.log("AWS SDK context", context.clientName, context.commandName);
    console.log("AWS SDK raw request", args.request);
    const result = await next(args);
    console.log("AWS SDK raw response", result.response);
    console.log("AWS SDK response metadata:", result.output.$metadata);
    return result;
  },
  {
    name: "MyMiddleware",
    step: "build",
    override: true,
  }
);

await client.listBuckets({});

Specifying priority

When you need to control the order of multiple middleware within the same step, use the priority option:
client.middlewareStack.add(middleware, {
  name: "MyMiddleware",
  step: "initialize",
  priority: "high", // or "low"
  override: true,
});

Client-level vs command-level middleware

Middleware added to client.middlewareStack applies to every command sent by that client. You can also add middleware to a single command instance’s middleware stack. It will apply only to that specific invocation:
const command = new GetObjectCommand({ Bucket: "my-bucket", Key: "my-key" });

command.middlewareStack.add(
  (next, context) => async (args) => {
    console.log("This middleware runs only for this one command instance");
    return next(args);
  },
  { step: "build", name: "PerCommandMiddleware" }
);

await client.send(command);
For more detail on the middleware stack design, see the AWS SDK blog post on middleware.

Build docs developers (and LLMs) love