Fundamentals

Adapters

Connectors that link your capabilities to the outside world.

What are adapters?

Adapters are the boundary between Routecraft and external systems. They handle the integration details (making HTTP calls, reading files, triggering on a schedule) so your capabilities stay focused on business logic.

Every capability starts with a source adapter in .from() and usually hands its result to a destination adapter in .to(). Operations in the middle can also use adapters to enrich data or observe side effects.

The adapter roles

Source

A source produces data and starts the flow. It goes in .from().

// Triggered by a timer
.from(timer({ interval: 60_000 }))

// One-shot with a fixed payload
.from(simple({ report: 'daily-summary' }))

// Receives messages from another capability (endpoint = route id)
.from(direct())

Destination

A destination pushes the exchange out to an external system. It goes in .to(). The push is void: the body flows through unchanged, and a receipt (a message id, an etag) lands on headers.

.to(log())
.to(json({ path: './output.json' }))
.to(jsonl({ path: './events.jsonl', append: true }))
.to(mail())

Enricher

An enricher pulls a value in per exchange: an HTTP GET, a file read, a lookup on another capability. It goes in .enrich(), where the fetched value replaces the body (or feeds an aggregator such as only() to merge). .to() accepts an enricher too: the result replaces the body there as well.

.enrich(http({ url: 'https://api.example.com/users/1' }))
.to(http({ method: 'POST', url: 'https://api.example.com/events' }))
.to(direct('next-stage'))

Processor

A processor sits in the middle of a pipeline and modifies the exchange. It goes in .process().

.process(myCustomProcessor)

Any destination or enricher can also be passed to .tap(). The .tap() operation is what makes it fire-and-forget: results and receipts are discarded, and the adapter itself is unchanged.

Configuring adapters

Most adapters accept an options object. Options can be static values or functions that derive a value from the exchange at runtime.

// Static
.to(http({ method: 'POST', url: 'https://api.example.com/events' }))

// Dynamic: derived from the exchange
.to(http({
  method: 'POST',
  url: (exchange) => `https://api.example.com/users/${exchange.body.userId}`,
  body: (exchange) => exchange.body,
}))

Merged options and craft config

Many adapters support merged options: they merge their own per-call options with context-level defaults set in craft.config.ts. This means you can define shared settings once and every adapter of that type picks them up automatically.

// craft.config.ts
import { defineConfig } from '@routecraft/routecraft'

export const craftConfig = defineConfig({
  cron: { timezone: 'UTC', maxJitter: 2000 },
})
// in a capability: timezone and maxJitter come from the config
.from(cron('@daily'))

// Override timezone for this specific source
.from(cron('0 9 * * 1-5', { timezone: 'America/New_York' }))

Options passed directly to the adapter always take precedence over config defaults. Merged options has the full pattern and the list of adapters that support it.


Adapters

Every adapter, each with its full signature, options and examples.

Merged options

Set adapter defaults once in craft.config.ts and share them across every capability.

Creating adapters

Build your own source, destination, enricher, or processor adapter.

Previous
Operations