How to Implement Middleware in AWS SDK for JavaScript v3: A Complete Guide
You implement middleware in AWS SDK for JavaScript v3 by calling client.middlewareStack.add() to inject custom logic at one of five pipeline steps—initialize, serialize, build, finalizeRequest, or deserialize—allowing you to intercept and modify requests before they reach the network layer.
The AWS SDK for JavaScript v3 (@aws-sdk/*) is built on a deterministic middleware stack architecture that processes every request through a defined pipeline. According to the aws/agent-toolkit-for-aws repository, you can implement middleware in AWS SDK for JavaScript v3 to perform request logging, inject custom headers, or implement retry logic by leveraging the middlewareStack property available on every client instance.
Understanding the Middleware Stack Architecture
When you create a client (e.g., S3Client, DynamoDBClient), it receives an empty middlewareStack that defines a five-step execution pipeline. As documented in plugins/aws-core/skills/aws-sdk-js-v3-usage/SKILL.md, the steps run in the following order:
- initialize – Prepare the command input before serialization.
- serialize – Convert the input into an HTTP request.
- build – Add headers, query strings, and other request components.
- finalizeRequest – Make final adjustments before sending, such as request signing.
- deserialize – Convert the HTTP response back into a typed output.
Each middleware function receives three arguments: next (the downstream handler), context (containing commandName, clientName, logger, and custom fields), and args (containing input, request, and other step-specific data). You register middleware using client.middlewareStack.add(), which accepts a handler function and a configuration object specifying the target step and execution order.
Implementing Middleware in AWS SDK for JavaScript v3
The following examples demonstrate how to implement middleware in AWS SDK for JavaScript v3 for common use cases, referencing patterns from the aws/agent-toolkit-for-aws source code.
Basic Logging Middleware (Build Step)
Add logging to inspect command inputs and outputs after the request is built but before signing. This example from plugins/aws-core/skills/aws-sdk-js-v3-usage/SKILL.md shows the standard pattern:
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
// Create a client – typically outside a Lambda handler for container reuse
const client = new S3Client({ region: "us-east-1" });
// Add a middleware that logs the command name and its input
client.middlewareStack.add(
(next, context) => async (args) => {
console.log("Running:", context.commandName, "with input:", args.input);
const result = await next(args); // Call the next middleware
console.log("Result:", result); // Optional post‑processing
return result;
},
{
name: "LogInputMiddleware",
step: "build", // Run after the request is built but before signing
override: true,
}
);
// Use the client as usual – the middleware runs automatically
await client.send(new GetObjectCommand({ Bucket: "my-bucket", Key: "my-key" }));
Adding Custom Headers (Initialize Step)
Inject headers or modify the input object before the SDK serializes it into an HTTP request. Using the initialize step ensures your logic runs first in the pipeline:
client.middlewareStack.add(
(next, context) => async (args) => {
// Mutate the input before the SDK serializes it
args.input.CustomHeader = "my‑value";
return next(args);
},
{
name: "CustomHeaderMiddleware",
step: "initialize", // Runs first – before serialization
priority: "low",
}
);
Conditional Retry Logic (FinalizeRequest Step)
Implement custom retry strategies by wrapping the next call in a try-catch block within the finalizeRequest step. This executes after request signing but before the network call:
client.middlewareStack.add(
(next, context) => async (args) => {
try {
return await next(args);
} catch (err) {
if (err.$metadata?.httpStatusCode === 429) {
// Simple exponential back‑off for throttling
await new Promise(r => setTimeout(r, 200));
return next(args); // Retry once
}
throw err;
}
},
{
name: "ThrottleRetryMiddleware",
step: "finalizeRequest",
priority: "high",
}
);
Creating Reusable Middleware Factories (TypeScript)
For larger applications, export middleware as reusable functions using the Pluggable interface from @aws-sdk/types. This pattern allows you to share middleware across multiple clients:
import type { Pluggable } from "@aws-sdk/types";
export const createTimingMiddleware = (label: string): Pluggable => ({
applyToStack: (clientStack) => {
clientStack.add(
(next, ctx) => async (args) => {
const start = Date.now();
const out = await next(args);
console.log(`${label} took ${Date.now() - start}ms`);
return out;
},
{ name: `${label}Timing`, step: "deserialize", override: true }
);
},
});
// Usage
client.middlewareStack.use(createTimingMiddleware("S3GetObject"));
Middleware Configuration and Ordering
When you implement middleware in AWS SDK for JavaScript v3, the second argument to add() accepts an object with the following properties, as detailed in plugins/aws-core/skills/aws-sdk-js-v3-usage/references/clients.md:
- name – A unique identifier used for later removal or debugging.
- step – The pipeline stage (
initialize,serialize,build,finalizeRequest, ordeserialize). - priority – Controls relative order; use
"high"to run earlier or"low"to run later within the same step. - override – When set to
true, replaces existing middleware with the same name.
The plugins/aws-core/skills/aws-sdk-js-v3-usage/references/effective-practices.md file provides guidance on endpoint middleware ordering, while plugins/aws-core/skills/aws-sdk-js-v3-usage/references/performance.md discusses the cacheMiddleware flag and when to disable it if your custom middleware conflicts with caching. For signature handling patterns, see plugins/aws-core/skills/aws-sdk-js-v3-usage/references/sigv4a.md.
Summary
- The AWS SDK for JavaScript v3 processes requests through five sequential steps: initialize, serialize, build, finalizeRequest, and deserialize.
- Use
client.middlewareStack.add()with a handler signature(next, context) => async args => { ... }to inject logic at any step. - Configure middleware with
name,step,priority, andoverrideoptions to control execution order and enable removal. - Middleware is client-wide, automatically applying to all commands issued by the client instance.
Frequently Asked Questions
What are the five steps in the AWS SDK for JavaScript v3 middleware pipeline?
The pipeline consists of initialize (prepare input), serialize (convert to HTTP request), build (add headers and query strings), finalizeRequest (signing and final adjustments), and deserialize (parse response). These steps execute sequentially for every command sent through the client.
How do I ensure my middleware runs before or after other middleware?
Use the priority option set to "high" or "low" when calling client.middlewareStack.add(). Higher priority middleware executes earlier within the same step. The override flag can also force replacement of existing middleware with the same name, as documented in plugins/aws-core/skills/aws-sdk-js-v3-usage/references/clients.md.
Can I remove middleware after adding it to the stack?
Yes. Because you assign a unique name when adding middleware, you can call client.middlewareStack.remove("YourMiddlewareName") to disable it dynamically. This is useful for temporarily disabling logging or debugging logic in production environments.
Does middleware affect all commands or only specific ones?
Middleware is client-wide, meaning it automatically runs for all commands issued by that client instance. If you need middleware for only specific operations, you must either check context.commandName inside your handler or create separate client instances with different middleware configurations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →