@ontologyio/receta
v0.3.5
Published
Practical FP recipes built on Remeda — higher-level patterns for real-world TypeScript applications
Maintainers
Readme
Receta
Practical FP recipes built on Remeda — Higher-level patterns for real-world TypeScript applications
What is Receta?
Receta (Spanish for "recipe") is a functional utility library that builds on top of Remeda. While Remeda provides low-level FP primitives (map, filter, pipe, etc.), Receta provides composed patterns that solve common real-world problems.
┌─────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────┤
│ Receta (Patterns & Recipes) │ ← This library
├─────────────────────────────────────┤
│ Remeda (FP Primitives) │
├─────────────────────────────────────┤
│ TypeScript / JS │
└─────────────────────────────────────┘Why Receta?
TypeScript gives you compile-time safety, but runtime is still the Wild West:
// ❌ THE VANILLA NIGHTMARE
async function getUserProfile(userId: string) {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const user = await response.json()
if (!user) throw new Error('User not found')
return user.profile?.email || null
} catch (error) {
console.error(error)
return null // 🤮 What went wrong? Who knows!
}
}7+ runtime failure points. Zero compile-time help. Good luck debugging production.
Receta Solution: Errors as Values + Pipe Composition
// ✅ RECETA: Type-safe, composable, self-documenting
import * as R from 'remeda'
import { Result, tryCatchAsync } from 'receta/result'
import { Option, fromNullable } from 'receta/option'
async function getUserProfile(
userId: string
): Promise<Result<string, FetchError>> {
return R.pipe(
await tryCatchAsync(
() => fetch(`/api/users/${userId}`).then(r => r.json()),
(e): FetchError => ({ type: 'network_error', cause: e })
),
Result.flatMap(user =>
Option.toResult(
fromNullable(user?.profile?.email),
{ type: 'email_not_found', userId }
)
)
)
}
// Caller knows EXACTLY what can fail
R.pipe(
await getUserProfile('123'),
Result.match({
Ok: (email) => console.log('Email:', email),
Err: (error) => {
if (error.type === 'network_error') showNetworkError()
if (error.type === 'email_not_found') showEmailMissing()
}
})
)Benefits:
- ✅ Compile-time exhaustiveness — TypeScript forces you to handle all error cases
- ✅ No hidden exceptions — Errors are explicit in return types
- ✅ Composable with
pipe— Chain operations without nested try/catch - ✅ Self-documenting — Function signature tells you what can fail
Installation
npm install receta remeda
# or
bun add receta remeda
# or
yarn add receta remedaQuick Start
Result: Type-Safe Error Handling
import { Result, ok, err, tryCatch } from 'receta/result'
import * as R from 'remeda'
// Parse JSON safely
const parseJSON = <T>(str: string): Result<T, SyntaxError> =>
tryCatch(
() => JSON.parse(str) as T,
(e) => e as SyntaxError
)
// Compose with pipe
const result = R.pipe(
'{"name": "Alice"}',
parseJSON,
Result.map((user: any) => user.name),
Result.unwrapOr('Unknown')
)
console.log(result) // "Alice"Option: No More Null/Undefined Bugs
import { Option, fromNullable } from 'receta/option'
import * as R from 'remeda'
type User = { settings?: { theme?: string } }
const getUserTheme = (user: User): string =>
R.pipe(
user.settings?.theme,
fromNullable,
Option.unwrapOr('light')
)
getUserTheme({}) // "light"
getUserTheme({ settings: { theme: 'dark' } }) // "dark"Async: Concurrency Control + Retry
import { mapAsync, retry } from 'receta/async'
// Fetch URLs with concurrency limit
const results = await mapAsync(
urls,
async (url) => fetch(url).then(r => r.json()),
{ concurrency: 5 } // Max 5 concurrent requests
)
// Retry with exponential backoff
const response = await retry(
() => fetch('https://api.example.com/data'),
{
maxAttempts: 3,
delay: 1000,
backoff: 'exponential' // 1s, 2s, 4s
}
)Predicate: Composable Filters
import { where, between, oneOf, gte } from 'receta/predicate'
import * as R from 'remeda'
const products = [
{ price: 50, inStock: true, category: 'electronics', rating: 4.5 },
{ price: 150, inStock: false, category: 'books', rating: 3.8 }
]
const filtered = R.filter(
products,
where({
price: between(10, 100),
inStock: true,
category: oneOf(['electronics', 'books']),
rating: gte(4.0)
})
)
// [{ price: 50, ... }]Validation: Form Validation with Error Accumulation
import { validate, combine, field } from 'receta/validation'
import { isEmail, minLength } from 'receta/string'
const validateRegistration = combine({
email: field('email', [
validate(isEmail, 'Must be a valid email')
]),
password: field('password', [
validate(minLength(8), 'Must be at least 8 characters')
])
})
const result = validateRegistration({
email: 'invalid',
password: '123'
})
// Invalid({ email: ['Must be a valid email'], password: ['Must be at least 8 characters'] })Core Modules
| Module | Purpose | Key Functions |
|--------|---------|---------------|
| result | Type-safe error handling | ok, err, tryCatch, match, unwrapOr |
| option | Nullable value handling | some, none, fromNullable, unwrapOr |
| async | Async utilities | mapAsync, retry, timeout, debounce, throttle |
| predicate | Composable predicates | where, gt, lt, between, oneOf, and, or |
| validation | Form/data validation | validate, combine, field, error accumulation |
| collection | Advanced collections | nest, diff, paginate, setOps |
| object | Object manipulation | flatten, unflatten, getPath, mask, deepMerge |
| string | String utilities | slugify, template, truncate, isEmail, sanitize |
| number | Number formatting | toCurrency, toBytes, clamp, percentage |
| memo | Memoization | memoize, memoizeAsync, TTL/LRU caches |
| lens | Immutable updates | prop, path, over, set, view |
| compare | Comparator builders | ascending, descending, natural, compose |
| function | Function combinators | ifElse, when, cond, compose, partial |
Design Principles
1. Compositional Architecture
- Functions are built from other functions, never duplicated
- Higher-level functions compose lower-level ones
- Single source of truth for each behavior
2. Remeda as Infrastructure
- Receta depends on and uses Remeda internally
- Re-exports Remeda utilities only when extending them
- Follows Remeda's data-first/data-last pattern via
purry
3. Result-First Error Handling
- Default: Functions return
Result<T, E>, not throw exceptions - Errors as values for explicit, composable error handling
- Throwing functions only when absolutely necessary
4. Type Safety First
- Leverage TypeScript's type system to the fullest
- All functions fully typed — no
any - Prefer narrowing over type assertions
5. Practical Over Academic
- Solve real problems, not theoretical exercises
- API reads like intent:
Result.tryCatch(() => JSON.parse(str)) - Optimize for the 90% use case, escape hatch for the 10%
6. Tree-Shakeable
- Each module can be imported independently
- No barrel files forcing bundling everything
- Side-effect free for dead code elimination
Documentation
- Why Receta? — Real-world problems and solutions
- Module Development Guide — Contributing new modules
- Module Guides:
- Result — Error handling patterns
- Option — Nullable value handling
- Async — Async utilities and patterns
- Predicate — Composable predicates
- Validation — Form and data validation
- Collection — Advanced collection operations
- Object — Safe object manipulation
- String — String utilities
- Number — Number formatting and math
- Memo — Memoization strategies
- Lens — Functional lenses
- Compare — Comparator builders
- Function — Function combinators
Examples
Check the examples/ directory for real-world usage patterns:
- Payment processing with error handling
- API request pipelines
- Form validation
- Data transformations
- Retry strategies
- And more!
When NOT to Use Receta
Receta is overkill for:
- Prototypes/scripts —
try/catchis fine for throwaway code - Simple CRUD — If your app is 90% database queries, simpler tools suffice
- Team unfamiliar with FP — Requires buy-in and learning curve
Use Receta when:
- Reliability matters — Payment processing, auth, data pipelines
- Error handling is complex — Multiple failure modes need distinct handling
- Type safety is critical — Financial apps, healthcare, aerospace
- Composability wins — Building reusable utilities and services
Contributing
We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup
- Testing guidelines
- Commit conventions
- Pull request process
License
MIT © 2026 Khaled Maher
Acknowledgments
- Built on Remeda — the best data-first/data-last FP library for TypeScript
- Inspired by Rust's
Result<T, E>andOption<T>types - Guided by practical FP principles from Scala, Haskell, and F#
Receta: Where TypeScript meets elegance. Stop fighting runtime errors. Start composing solutions.
