@skein-js/nestjs
v0.9.0
Published
NestJS adapter for skein-js — serve the Agent Protocol from a Nest module.
Maintainers
Readme
@skein-js/nestjs
NestJS adapter for skein-js — serve the Agent Protocol from a Nest module.
Part of skein-js — a TypeScript Agent Protocol server for LangGraph.js, and a drop-in replacement for the LangGraph CLI.
A thin transport shim over the framework-agnostic @skein-js/agent-protocol
handler table — it adds no protocol logic, exactly like @skein-js/express.
SkeinModule mounts the protocol as middleware: it claims skein's paths and passes every other
request through to your own controllers, so it composes cleanly with an existing app.
Platform: targets NestJS's default Express platform (
@nestjs/platform-express).
Install
npm i @skein-js/nestjs @nestjs/common @nestjs/core @nestjs/platform-express @langchain/langgraphEmbedded in an existing app
import { Module } from "@nestjs/common";
import { SkeinModule } from "@skein-js/nestjs";
@Module({
imports: [SkeinModule.forRoot({ config: "./langgraph.json" })],
controllers: [/* your own controllers */],
})
export class AppModule {}The Agent Protocol is now served (/threads, /assistants, /runs, /store, …) alongside your
routes. Call app.enableCors(...) as usual if browser clients run on another origin. Enable shutdown
hooks (app.enableShutdownHooks()) so the background run worker drains on exit.
Serving under a global prefix
If your app calls app.setGlobalPrefix(...), the protocol follows it — there is nothing to configure
on the skein side. Two things to do:
1. Set the prefix as you normally would, and write your own controllers prefix-relative:
@Controller("todos") // Nest serves this at /api/todos
class TodosController {}
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix("api"); // the protocol moves to /api too
await app.listen(2024);2. Point your client at the prefixed root — not the server root:
const client = new Client({ apiUrl: "http://localhost:2024/api" });That's it. /api/threads, /api/assistants, /api/runs, /api/store/items all serve, and requests
that aren't skein's still fall through to your controllers.
Still getting 404s?
Unsupported route path: "/api/*"in your boot log — expected and harmless. Nest logs it while auto-converting the adapter's catch-all to NestJS 11 wildcard syntax; the conversion succeeds. It is not the cause of a 404.- Requests to the server root 404 — correct once a prefix is set.
POST /threadsis not served when the prefix isapi; use/api/threads. GET /apiitself 404s — expected, and not a sign the mount is wrong. Nest does not route the bare prefix root to middleware (nestjs/nest#14520). No protocol route lives there; probe/api/assistants/searchinstead./info404s — also correct. It isn't part of the Agent Protocol surface skein serves; the endpoints are/threads,/assistants,/runsand/store/items. NoteGET /okis only mounted by the standalonecreateNestServer—SkeinModuleadds no health route to your app.- Everything 404s and you're on an older release — the protocol ignored
setGlobalPrefixbefore this was fixed, so every path 404'd under a prefixed app. Upgrade, or drop the prefix and mount your own controllers at@Controller("api/…")instead.
No langgraph.json? Pass a graph you already have
{ deps } is the alternative to { config }: bring a compiled graph straight from your code — no
config file, no CLI. embedInMemoryGraphs turns a graph map into the
ProtocolDeps the module needs:
import { Module } from "@nestjs/common";
import { SkeinModule } from "@skein-js/nestjs";
import { embedInMemoryGraphs } from "@skein-js/server-kit";
import { agent } from "./graphs/agent-graph";
@Module({
imports: [SkeinModule.forRoot({ deps: embedInMemoryGraphs({ agent }) })],
controllers: [/* your own controllers */],
})
export class AppModule {}Map keys become graph ids. For durable state, swap in embedPostgresGraphs (Postgres + Redis) from
@skein-js/runtime — or, if you do have a langgraph.json and just want production
drivers, its buildRuntime. Full walkthrough: docs/embedding.md.
Graphs as plain endpoints (non-chat)
For workloads that aren't chat — a classifier, an extractor, a workflow another service calls — there
is a smaller surface: every graph mounted as POST /invoke/:graph_id, where the request body is
the graph input and the response is the final state. No threads, assistants, or runs.
import { SkeinInvokeModule } from "@skein-js/nestjs";
@Module({ imports: [SkeinInvokeModule.forRoot({ deps })] })
export class AppModule {}Send Accept: text/event-stream to stream the steps instead. See
docs/serving-a-single-graph.md.
Standalone server
A dedicated server whose only job is to serve your graphs:
import { createNestServer } from "@skein-js/nestjs";
const server = await createNestServer({ config: "./langgraph.json" });
await server.listen(2024);
// on shutdown: await server.close(); // stops the run workerThe same { deps } seam applies here — createNestServer({ deps: embedInMemoryGraphs({ agent }) })
serves a graph you hold in code, with no langgraph.json on disk.
Streaming
SSE responses write directly to the raw Node response and stream the pre-serialized frames the engine produced, tearing the run's subscription down on client disconnect.
API
SkeinModule.forRoot(options): DynamicModule— the primary entry point;imports: [...]it to mount the protocol as middleware alongside your controllers.optionsisSkeinRuntimeOptions.SkeinMiddleware— the underlying Nest middleware, for callers wiring their own module.createNestServer(options): Promise<SkeinNestServer>— a standalone server;SkeinNestServer={ app, runtime, listen(port?, host?), close() }.close()closes the Nest app, which stops the run worker via the module's shutdown hook.SKEIN_RUNTIME/SKEIN_LOGGER/SKEIN_CORS— DI tokens; injectSKEIN_RUNTIMEto reach theResolvedProtocolRuntimefrom your own providers — its.runtimeis theProtocolRuntime(assistants, handlers, worker), plus.cors.SkeinInvokeModule.forRoot(options): DynamicModule— the simplified serving surface:POST /invoke/:graph_idper graph, body-in / final-state-out, for non-chat workloads. Options addprefix(default/invoke) andstreamMode. Also exportsSkeinInvokeMiddlewareand theSKEIN_INVOKEtoken.SkeinRuntimeOptions— the shared seam every adapter accepts: common{ logger?, cors?, warm? }plus either{ config, importModule? }(in-memory runtime from alanggraph.json) or{ deps }(bring-your-ownProtocolDeps). Builddepsin code withembedInMemoryGraphs(@skein-js/server-kit) orembedPostgresGraphs(@skein-js/runtime), or from alanggraph.jsonwith that package'sbuildRuntime.- Low-level mappers:
toProtocolRequest, plussendNodeResponse/sendNodeError(re-exported from@skein-js/server-kit).
Learn more
@skein-js/express— the reference adapter- Embedding a graph you already have — the
{ deps }path, nolanggraph.json - Serving a graph as a plain endpoint — the non-chat surface
- Building your own adapter · skein-js overview
