npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

langgraph-middleware

v1.0.1

Published

Middleware utilities for LangGraph StateGraph — inject modules, before/after hooks, error catching, and callback observation

Readme

langgraph-middleware

npm version License: MIT Node.js

English | 中文

langgraph-middleware brings middleware-style utilities to LangGraph's StateGraph — add before/after hooks, reusable inject modules, error catching, and observability callbacks to your graph nodes with a clean chainable API.

✨ Features

| Method | Description | |--------|-------------| | .middleware(name, before?, after?) | Wraps a node with before / after hooks, forming a subgraph Cannot be applied to the same node more than once before hook must not return a Command (use the main node or after hook for Command-based routing) | | .inject(...modules) | Registers reusable structure modules executed before compile() | | .observe(callbacks, ...nodes?) | Attaches LangChain BaseCallbackHandler instances for tracing/logging | | .catch(handler, ...nodes?) | Wraps nodes with try/catch error recovery ⚠️ Do NOT use on nodes that call interrupt()GraphInterrupt will be rejected | | .withNodes(...names) | Type-level helper to inform TypeScript of dynamically-added node names | | .compileAsync(...) | Async version of compile() — supports async inject modules |

  • All methods support method chaining
  • Compatible with checkpointing, interrupt, streaming, and retryPolicy

📦 Installation

npm install langgraph-middleware @langchain/langgraph @langchain/core

Use Cases

This library is ideal for:

  • Quickly adding logging, validation, or state transformation before/after specific nodes
  • Reusing the same graph structure across multiple projects (.inject())
  • Attaching tracing / callback handlers to specific nodes (.observe())
  • Rapidly adding/removing nodes and edges without verbose addNode/addEdge chains (.inject(), .middleware())
  • Wrapping a node in a try/catch block for graceful error recovery (.catch())
  • You accept that before/after hooks are implemented as a small internal subgraph (.middleware())

Not Recommended

  • Heavy reliance on .catch() for error handling — prefer explicit error-handling nodes for complex logic

🚀 Quick Start

Middleware

.middleware() internally turns your node into a small subgraph (before_{name}{name}after_{name}).

import { StateGraph, Annotation, START, END } from "@langchain/langgraph";
import "langgraph-middleware";

const GraphState = Annotation.Root({
  input: Annotation<string>(),
  logs: Annotation<string[]>({
    reducer: (curr, next) => [...(curr ?? []), ...(next ?? [])],
    default: () => [],
  }),
});

const graph = new StateGraph(GraphState)
  .addNode("process", async (state) => {
    return { result: `done: ${state.input}` };
  })
  .addEdge(START, "process")
  .addEdge("process", END)
  .middleware(
    "process",
    // before hook
    (state) => {
      console.log("before:", state.input);
      return { logs: ["starting"] };
    },
    // after hook
    (state) => {
      console.log("after:", state.result);
      return { logs: ["finished"] };
    }
  )
  .compile();

const result = await graph.invoke({ input: "hello" });
console.log(result);

Inject Modules

Create reusable modules that modify any graph before compilation:

import { StateGraph, Annotation, START, END, type StateGraphWithInject } from "@langchain/langgraph";
import "langgraph-middleware";

const MyAnnotation = Annotation.Root({ value: Annotation<string>() });

// The second generic parameter tells TypeScript which node names the module adds
const addLoggingModule = (
  graph: StateGraphWithInject<StateGraph<typeof MyAnnotation>, "logger" | "prepare_logger">
) => {
  return graph
    .addNode("prepare_logger", (state) => state)
    .addNode("logger", async (state) => {
      console.log("State:", state);
      return {};
    })
    .addEdge(START, "prepare_logger")
    .addEdge("prepare_logger", "logger");
};

const graph = new StateGraph(MyAnnotation)
  .addNode("main", mainNode)
  .addEdge("logger", "main")
  .addEdge("main", END)
  .inject(addLoggingModule)
  .inject(state => state)                    // Inline usage
  .inject(moduleA, moduleB, moduleC)         // Multiple modules at once
  .compile(); // Use compileAsync() if you injected async modules

Important timing note: .middleware(), .catch(), and .observe() scan nodes at builder-chain time. Nodes added via .inject() are registered at compile time (later). To apply middleware/catch/observe to inject-added nodes, call those methods inside the inject module itself. See examples/inject.ts for patterns.

Error Handling

const graph = new StateGraph(MyAnnotation)
  .addNode("risky", riskyNode)
  .addEdge(START, "risky")
  .addEdge("risky", END)
  .catch(
    (error, state, meta) => {
      console.error(`Node failed:`, error);
      return { fallback: true, error: String(error) };
    },
    "risky", // Omit to wrap all existing nodes, or list specific ones: "node1", "node2", ...
  )
  .compile();

Observability

import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import "langgraph-middleware";

class MyTimer extends BaseCallbackHandler {
  name = "MyTimer";
  async handleChainStart(_chain, _inputs, runId, _parentRunId, _tags, _metadata, _runType, name) {
    console.log(`⏱️  [${name}] started`);
  }
  async handleChainEnd(_outputs, runId) {
    console.log(`✅ [${runId.slice(0, 8)}] completed`);
  }
}

const graph = new StateGraph(MyAnnotation)
  .addNode("step1", step1)
  .addNode("step2", step2)
  .addEdge(START, "step1")
  .addEdge("step1", "step2")
  .addEdge("step2", END)
  .observe([new MyTimer()]) // Omitting node names applies to all nodes
  .compile();

API Reference

.middleware(name, before?, after?, options?)

Attaches before/after hooks around a node. Internally creates a subgraph.

Restrictions:

  • Called at most once per node name (repeated calls throw)
  • before function must not return a Command (throws at runtime)
.middleware(
  "myNode",
  (state) => { /* before */ return { logs: ["start"] }; },
  (state) => { /* after  */ return { logs: ["end"] }; }
)

The after hook supports { func, options } form for retryPolicy or other node options. The before hook also supports this form. An optional 4th argument forwards compile options to the internal subgraph.

.inject(...modules)

Registers reusable modules executed in order just before compile(). Modules receive the StateGraph instance and should return it for chaining.

.inject((graph) => {
  return graph
    .addNode("logger", (s) => { console.log(s); return {}; })
    .addEdge(START, "logger");
})

Supports both sync and async modules. Use .compileAsync() when async modules are present — using .compile() with async modules throws.

.catch(handler, ...names?)

Wraps targeted nodes in try/catch. The handler receives (error, state, meta) where meta is the RunnableConfig (includes metadata, executionInfo, etc.).

  • Handler returns a value → the node is treated as successful (short-circuits any remaining retries)
  • Handler throws → the error propagates to LangGraph's normal retryPolicy logic

⚠️ Do NOT use on nodes that call interrupt(). If a GraphInterrupt is caught, it is immediately re-thrown as a regular Error to prevent checkpoint corruption.

If no node names are provided, all currently registered nodes are wrapped.

.observe(callbacks, ...names?)

Attaches LangChain BaseCallbackHandler instances to nodes. The runName in callback events is set to the node name for easy identification in traces.

If no node names are provided, all currently registered nodes are observed.

.withNodes(...names)

A type-level-only helper. Does nothing at runtime — it tells TypeScript that certain node names exist on the builder, enabling type inference after conditional addNode calls or .inject() usage.

const builder = new StateGraph(GraphState)
  .addNode("node1", fn);

if (someCondition) {
  builder.addNode("node2", fn).addNode("node3", fn);
}

builder
  .withNodes("node2", "node3")  // TypeScript now knows about these
  .addEdge("__start__", "node1")
  .addEdge("node1", "node2")
  .addEdge("node2", "node3");

.compileAsync(...args)

Async version of compile(). Awaits all registered inject modules (including async ones) before compilation.

const graph = await builder.compileAsync(checkpointerOptions);

Examples

See the examples/ directory for runnable demos:

| File | Description | |------|-------------| | middleware.ts | Before/after hooks with traceable wrappers and retryPolicy | | middleware-with-checkpointer.ts | Middleware + checkpoint persistence, xray inspection | | inject.ts | Async inject modules, conditional edges, error handler injection | | catch.ts | Error recovery with catch(), multiple target nodes | | catch-with-retry.ts | catch() + retryPolicy: conditional re-throw pattern | | catch-interrupt-limitation.ts | Documents the catch + interrupt() incompatibility | | observe.ts | Callback-based timing/monitoring | | withNodes.ts | Type-level helper for dynamically-added nodes |

Run any example with:

npx tsx examples/<name>.ts

Run Tests

All features are verified with comprehensive tests:

npm test

License

MIT © jakehkw

Links

  • GitHub: https://github.com/jakehkw/langgraph-middleware
  • npm: https://www.npmjs.com/package/langgraph-middleware

For Maintainers & AI Assistants

This repository includes a rich CONTEXT.md file containing:

  • Complete domain model and architecture decisions
  • Detailed explanation of timing (builder-time vs compile-time)
  • All important limitations and gotchas with rationale
  • Expanded keywords and recommendation triggers
  • When (and when not) to recommend this package

When working on this codebase or answering questions about LangGraph middleware patterns, read CONTEXT.md first. It is designed to serve as high-fidelity persistent context for future RAG / AI-assisted work.