klubok
v0.5.4
Published
Do notation pipes for Promise-based or pure functions which easy to mock
Maintainers
Readme
Klubok

Do-notation pipes for Promise-based or pure functions with easy mocking.
Inspired by fp-ts/Effect bind Do-notation, but smaller and simpler. Primarily created for easy mocking of functions, allowing you to write extensive unit tests using the London school approach.
Features
- 🔄 Sequential composition — Chain pure and async functions with automatic context passing
- 🧪 Easy mocking — Mock any step in the pipeline without complex dependency injection
- 🎯 Selective execution — Run only specific functions using the
onlyparameter - 🛡️ Type-safe — Full TypeScript support with inferred types for up to 20 functions
- 🐛 Debug-friendly — Automatic context attachment to errors for easier debugging
- ⚡ Mutable state — Support for mutable operations when needed
Installation
npm install klubokQuick Start
import { pure, eff, klubok } from 'klubok'
const catsBirthdays = klubok(
eff('cats', async ({ ids }: { ids: number[] }) => {
/* fetch cats from DB */
}),
pure('catsOneYearOld', ({ cats }) =>
cats.map(cat => ({ ...cat, age: cat.age + 1 }))
),
eff('saved', async ({ catsOneYearOld }) => {
/* save to DB */
})
)
// Production usage
catsBirthdays({ ids: [1, 2, 3] })
// In tests: mock 'cats' and run only 'catsOneYearOld'
catsBirthdays(
{ ids: [1, 2, 3] },
{
// DB response mock
cats: () => [
{ name: 'Barsik', age: 10 },
{ name: 'Marfa', age: 7 }
]
},
// call only this function
['catsOneYearOld']
)
// Promise<{ cats: [...], catsOneYearOld: [{ name: 'Barsik', age: 11 }, ...] }>API Reference
pure<K, C, R>(key: K, fn: (ctx: C) => R)
Creates a synchronous keyed function for the pipeline.
import { pure, klubok } from 'klubok'
const fn = klubok(
pure('inc', (ctx: { number: number }) => ctx.number + 1),
pure('str', ({ inc }) => inc.toString())
)
await fn({ number: 1 })
// { number: 1, inc: 2, str: '2' }eff<K, C, R>(key: K, fn: (ctx: C) => Promise<R>)
Creates an asynchronous (Promise-based) keyed function for the pipeline.
import { eff, klubok } from 'klubok'
const fn = klubok(
eff('fetchUser', async ({ id }: { id: number }) => {
return { id, name: 'Alice' }
}),
eff('fetchPosts', async ({ user }) => {
return [{ title: 'Hello' }]
})
)
await fn({ id: 1 })
// { id: 1, user: {...}, posts: [...] }mut(fn: KeyedFunction)
Marks a function as mutable, allowing it to override a previous result with the same key.
import { pure, mut, klubok } from 'klubok'
const fn = klubok(
pure('data', () => [1, 2, 3]),
mut(pure('data', ({ data }) => data.map(x => x * 2)))
)
await fn({})
// { data: [2, 4, 6] }klubok(...fns)
Main function that composes keyed functions into a pipeline. Returns a function with the signature:
(
ctx: C,
mock?: { [key: string]: value | ((ctx) => value | Promise<value>) },
only?: string[]
) => Promise<{ ...ctx, ...results }>Parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| ctx | object | Initial context (input data) |
| mock | object (optional) | Mock implementations for any step. Values can be static or functions |
| only | string[] (optional) | Execute only specified keys, skipping others |
Mocking
Static Mocks
Replace a function's result with a static value:
const fn = klubok(
eff('fetchData', async () => {
/* real API call */
}),
pure('process', ({ fetchData }) => fetchData.map(x => x * 2))
)
await fn({}, { fetchData: [1, 2, 3] })
// { fetchData: [1, 2, 3], process: [2, 4, 6] }Function Mocks
Replace a function with a custom implementation (sync or async):
await fn(
{},
{
fetchData: () => [1, 2, 3],
process: ({ fetchData }) => fetchData.filter(x => x > 1)
}
)Mocks with Context Access
Mock functions receive the accumulated context:
await fn(
{ userId: 42 },
{
fetchData: ({ userId }) => {
console.log('Mock called with userId:', userId)
return [{ id: userId, value: 100 }]
}
}
)Selective Execution (only)
Run only specific functions in the pipeline:
const fn = klubok(
eff('step1', async () => { /* ... */ }),
pure('step2', ({ step1 }) => step1 * 2),
eff('step3', async ({ step2 }) => { /* ... */ })
)
// Execute only step1 and step2
await fn({}, {}, ['step1', 'step2'])
// { step1: ..., step2: ... } — step3 is skippedUseful for unit testing individual transformations without running the entire pipeline.
Error Handling
Automatic Context Attachment
When an error is thrown, Klubok automatically attaches the current context to the error stack:
const fn = klubok(
pure('step1', () => 42),
eff('step2', async ({ step1 }) => {
throw new Error('Failed!')
})
)
try {
await fn({})
} catch (e) {
console.log(e.stack)
// Error: Failed!
// at ...
// context: { step1: 42 }
}Approved Errors
Errors with isApproved = true property won't have context attached (useful for expected business logic errors):
class ApprovedError extends Error {
isApproved = true
constructor(message: string, options?: ErrorOptions) {
super(message, options)
}
}
const fn = klubok(
eff('validate', async () => {
throw new ApprovedError('Invalid input')
})
)Type Safety
Klubok provides full TypeScript inference for pipelines up to 20 functions. Each step's output type is automatically added to the context type for subsequent steps:
const fn = klubok(
pure('a', (ctx: { x: number }) => ctx.x + 1), // ctx: { x: number }
pure('b', ({ a }) => a.toString()), // ctx: { x: number, a: number }
pure('c', ({ a, b }) => a + b.length) // ctx: { x: number, a: number, b: string }
)
// Result: Promise<{ x: number, a: number, b: string, c: number }>Use Cases
Testing with London School Approach
Mock external dependencies at any level without changing production code:
// production.ts
export const processOrder = klubok(
eff('fetchOrder', async ({ orderId }) => db.orders.find(orderId)),
eff('validateStock', async ({ fetchOrder }) => warehouse.check(fetchOrder)),
eff('chargePayment', async ({ fetchOrder }) => paymentGateway.charge(fetchOrder)),
eff('shipItems', async ({ fetchOrder, chargePayment }) => shipping.create(fetchOrder))
)
// test.ts
await processOrder(
{ orderId: 123 },
{
fetchOrder: () => ({ id: 123, items: ['item1'] }),
validateStock: () => true
},
['chargePayment'] // Test only payment logic
)Data Transformation Pipelines
Chain pure transformations with clear, testable steps:
const transformData = klubok(
pure('parsed', ({ raw }: { raw: string }) => JSON.parse(raw)),
pure('validated', ({ parsed }) => schema.parse(parsed)),
pure('normalized', ({ validated }) => normalize(validated)),
pure('enriched', ({ normalized }) => addMetadata(normalized))
)See Also
- Klubok-Gleam — Gleam implementation of Klubok
License
MIT © Vladislav Botvin
