# @effectionx/context-api

> Algebraic effects pattern for context-dependent operations with middleware

Latest version **0.6.0** (published 2026-04-10) · MIT license · 0 weekly downloads

## Install

```sh
npm install @effectionx/context-api
pnpm add @effectionx/context-api
yarn add @effectionx/context-api
bun add @effectionx/context-api
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; has provenance; high maintenance score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.6.0 |
| Published | 2026-04-10 |
| First published | 2025-08-07 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 1 |
| Unpacked size | 72.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Provenance | attested (GitHub Actions) |
| GitHub stars | 12 |
| Author | engineering@frontside.com |
| Maintainers | frontsidejack |
| Keywords | concurrency, interop |

## Links

- npm: https://www.npmjs.com/package/@effectionx/context-api
- Repository: https://github.com/thefrontside/effectionx
- Homepage: https://github.com/thefrontside/effectionx#readme
- Issues: https://github.com/thefrontside/effectionx/issues
- npm.io page: https://npm.io/package/@effectionx/context-api

## Dependencies (1)

- [@effectionx/middleware](https://npm.io/package/@effectionx/middleware.md) 0.1.1

## Recent versions

- 0.6.0 (latest) — 2026-04-10
- 0.5.3 — 2026-03-29
- 0.5.2 — 2026-03-22
- 0.5.1 — 2026-03-20
- 0.4.0 — 2026-03-13
- 0.3.2 — 2026-02-23
- 0.3.1 — 2026-02-12
- 0.3.0 — 2026-01-18
- 0.2.1 — 2025-12-08
- 0.2.0 — 2025-12-08
- 0.1.0 — 2025-08-22
- 0.0.2 — 2025-08-08
- 0.0.1 — 2025-08-07

## README

# Context APIs

Algebraic effects pattern for context-dependent operations with middleware

---

Often called "Algebraic Effects" or "Contextual Effects", Context APIs let you
access an operation via the context in a way that it can be easily (and
contextually) wrapped with middleware. Middleware is powered by
[`@effectionx/middleware`](../middleware/README.md) and supports min/max priority
ordering.

## Quick Start

Let's say that you want to define a log operation that behaves differently in
different contexts. The basic form will just log values to the console.

```ts
import { createApi } from "@effectionx/context-api";

export const logging = createApi("logging", {
  *log(...values: unknown[]) {
    console.log(...values);
  },
});

export const { log } = logging.operations;
```

Now you can use the logging API wherever you want:

```ts
import { log } from "./logging.ts";

export function* op() {
  yield* log("I am in an operation");
}
```

## Wrapping with Middleware

Use the `around` function to wrap middleware around your operations. This lets
you intercept calls, transform arguments, modify return values, or replace
the implementation entirely.

```ts
import { logging } from "./logging.ts";

function* initCustomLogging(externalLogger: { log(...values: unknown[]): void }) {
  yield* logging.around({
    *log([...values], next) {
      externalLogger.log(...values);
      // since we override the logger entirely, we do not invoke next
    },
  });
}
```

Middleware is only in effect inside the scope in which it is installed — when
the scope exits, the middleware is removed.

## Min/Max Priority

By default, `around()` registers middleware at `"max"` priority (outermost,
closest to the caller). You can also register at `"min"` priority (innermost,
closest to the core handler) by passing an options argument:

```ts
import { createApi } from "@effectionx/context-api";
import type { Operation } from "effection";

export const files = createApi("files", {
  *readFile(path: string): Operation<string> {
    throw new Error(`readFile("${path}") is not implemented`);
  },
});

export const { readFile } = files.operations;
```

In your runtime setup, provide the implementation via `min`:

```ts
import { files } from "./files.ts";

function* initNodeRuntime() {
  yield* files.around(
    {
      *readFile([path], _next) {
        return yield* nodeReadFile(path);
      },
    },
    { at: "min" },
  );
}
```

`max` middlewares wrap the outside as usual — they don't care which `min` is
providing the actual implementation:

```ts
import { files } from "./files.ts";

function* withLogging() {
  yield* files.around({
    *readFile([path], next) {
      console.log(`reading ${path}`);
      return yield* next(path);
    },
  });
}
```

In tests, swap the implementation by registering a different `min`:

```ts
function* useTestFixtures(fixtures: Map<string, string>) {
  yield* files.around(
    {
      *readFile([path], _next) {
        return fixtures.get(path) ?? "";
      },
    },
    { at: "min" },
  );
}
```

The execution order with max middlewares `[M1, M2]` and min middlewares
`[m1, m2]` is:

```text
M1 → M2 → m1 → m2 → core
```

## Instrumentation

Middleware can be useful for automatic instrumentation:

```ts
import { fetching } from "./fetching.ts";

function* instrumentFetch(tracer) {
  yield* fetching.around({
    *fetch(args, next) {
      try {
        tracer.begin("fetch", args);
        return yield* next(...args);
      } finally {
        tracer.end("fetch", args);
      }
    },
  });
}
```

## Test Mocking

Mock operations in test cases without changing the call site:

```ts
import { fetching } from "./fetching.ts";

function* useMocks() {
  yield* fetching.around({
    *fetch([url, ...rest], next) {
      if (url === "/my-path") {
        return new MockResponse("my-path");
      } else {
        return yield* next(url, ...rest);
      }
    },
  });
}
```

## Scope Isolation

Middleware installed in a child scope does not affect the parent:

```ts
import { scoped } from "effection";
import { log, logging } from "./logging.ts";

function* example() {
  yield* scoped(function* () {
    yield* logging.around({
      *log([...values], next) {
        // only active inside this scope
        return yield* next(...values);
      },
    });
    yield* log("intercepted"); // middleware runs
  });

  yield* log("not intercepted"); // middleware does not run
}
```

## API

### `createApi(name, handler)`

Create a context API from a name and an object of handler functions or
operations. Returns an object with `operations` and `around`.

```ts
import { createApi } from "@effectionx/context-api";
import type { Operation } from "effection";

const math = createApi("math", {
  *add(left: number, right: number): Operation<number> {
    return left + right;
  },
});

const { add } = math.operations;

function* example(): Operation<void> {
  const result = yield* add(1, 2); // => 3
}
```

### `around(middlewares, options?)`

Register middleware around one or more operations. The second argument controls
priority:

- **`{ at: "max" }`** (default) — outermost, closest to the caller
- **`{ at: "min" }`** — innermost, closest to the core handler

```ts
function* example() {
  // Wrapping middleware (max, default)
  yield* math.around({
    *add(args, next) {
      console.log("adding", args);
      return yield* next(...args);
    },
  });

  // Implementation middleware (min)
  yield* math.around(
    {
      *add([left, right], _next) {
        return left * right; // replace the core implementation
      },
    },
    { at: "min" },
  );
}
```

Each middleware receives the arguments as a tuple and a `next` function to
delegate to the next middleware (or the core handler). A middleware can:

- **Pass through**: call `next(...args)` and return its result
- **Transform arguments**: call `next()` with different arguments
- **Transform the return value**: modify what `next()` returns
- **Short-circuit**: return a value without calling `next()` at all

---
_Source: https://npm.io/package/@effectionx/context-api · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
