Object
Patterns & Recipes
Copy-paste ready patterns for common real-world scenarios. All examples are production-ready and based on actual API structures.
Table of Contents
API Integration
Pattern: GitHub API Normalization
import { map } from '@ontologyio/receta/result'
import * as Obj from '@ontologyio/receta/object'
import { pipe } from 'remeda'
// GitHub repository response → Your schema
const normalizeGitHubRepo = (apiResponse: unknown) =>
pipe(
apiResponse,
Obj.validateShape({
name: (v: unknown): v is string => typeof v === 'string',
full_name: (v: unknown): v is string => typeof v === 'string',
owner: {
login: (v: unknown): v is string => typeof v === 'string',
avatar_url: (v: unknown): v is string => typeof v === 'string'
},
stargazers_count: (v: unknown): v is number => typeof v === 'number',
created_at: (v: unknown): v is string => typeof v === 'string'
}),
map(Obj.rename({
full_name: 'fullName',
stargazers_count: 'stars',
created_at: 'createdAt'
})),
map((repo) => Obj.flatten(repo)),
map((flat) => Obj.rename(flat, {
'owner.login': 'ownerUsername',
'owner.avatar_url': 'ownerAvatar'
})),
map((renamed) => Obj.unflatten(renamed))
)Pattern: Stripe Payment Intent Processing
import { toResult } from '@ontologyio/receta/option'
import { map as mapResult } from '@ontologyio/receta/result'
const cardBrandKey = 'charges.data.0.payment_method_details.card.brand'
// Stripe API → Your database schema
const processPaymentIntent = (stripeResponse: Obj.PlainObject) =>
pipe(
stripeResponse,
Obj.getPath(['id']),
toResult({ code: 'MISSING_ID' }),
mapResult(() =>
pipe(
Obj.flatten(stripeResponse, { flattenArrays: true }),
Obj.filterKeys((key) =>
['id', 'amount', 'currency', 'status', 'customer.email', cardBrandKey].some(
(allowed) => key.startsWith(allowed)
)
),
Obj.rename({
'customer.email': 'customerEmail',
[cardBrandKey]: 'cardBrand'
})
)
)
)Form Handling
Pattern: React Form State Manager
import { type Option } from '@ontologyio/receta/option'
import { type Result } from '@ontologyio/receta/result'
interface FormState<T> {
data: T
errors: Record<string, string>
touched: Record<string, boolean>
}
class Form<T extends Obj.PlainObject> {
constructor(private state: FormState<T>) {}
// Get field value
getValue<V>(path: Obj.ObjectPath): Option<V> {
return Obj.getPath<V>(path, this.state.data)
}
// Update field
updateField(path: Obj.ObjectPath, value: unknown): Form<T> {
return new Form({
...this.state,
data: Obj.setPath(path, value, this.state.data),
touched: Obj.setPath(path, true, this.state.touched)
})
}
// Validate and submit
submit(schema: Obj.ObjectSchema<T>): Result<T, Obj.ObjectError> {
return Obj.validateShape(Obj.compact(this.state.data), schema)
}
}
// Usage
const userForm = new Form({
data: { name: '', email: '', age: undefined },
errors: {},
touched: {}
})
userForm
.updateField(['name'], 'Alice')
.updateField(['email'], 'alice@example.com')
.submit(userSchema)Pattern: Multi-Step Form with Partial Validation
import { flatMap } from '@ontologyio/receta/result'
const multiStepForm = {
// Step 1: Personal Info
validateStep1: (data: Obj.PlainObject) =>
pipe(
data,
Obj.mask(['name', 'email', 'phone']),
Obj.stripUndefined,
(partial) => Obj.validateShape(partial, step1Schema)
),
// Step 2: Address
validateStep2: (data: Obj.PlainObject) =>
pipe(
data,
Obj.mask(['street', 'city', 'state', 'zip']),
Obj.stripUndefined,
(partial) => Obj.validateShape(partial, step2Schema)
),
// Final: Combine and validate all
submitAll: (formData: Obj.PlainObject) =>
pipe(
formData,
Obj.compact,
(data) => Obj.validateShape(data, fullSchema),
flatMap(submitToAPI)
)
}Configuration Management
Pattern: Environment-Specific Config Merge
import { unwrapOr } from '@ontologyio/receta/result'
// Next.js / Vite style config merging
const loadConfig = () => {
const base = {
api: {
url: 'https://api.example.com',
timeout: 30000,
retries: 3
},
features: {
analytics: false,
debugMode: false
}
}
const envConfig = pipe(
process.env,
Obj.filterKeys((key) => key.startsWith('APP_')),
Obj.mapKeys((key) => key.replace(/^APP_/, '').toLowerCase()),
(flat) => Obj.unflatten(flat, { separator: '__' })
)
const userConfig = loadUserConfig() // From file
return pipe(
[base, envConfig, userConfig],
Obj.deepMerge({ arrayStrategy: 'concat' }),
(merged) => Obj.validateShape<typeof base>(merged, configSchema),
unwrapOr(base)
)
}Pattern: Feature Flags with Fallbacks
import { map, unwrapOr } from '@ontologyio/receta/option'
const getFeatureFlags = (remoteConfig: Obj.PlainObject) => {
const defaults = {
newDashboard: false,
betaFeatures: false,
experimentalApi: false
}
return pipe(
remoteConfig,
Obj.getPath<Record<string, boolean>>(['features']),
map(Obj.compact),
map((flags) => ({ ...defaults, ...flags })),
unwrapOr(defaults)
)
}Security & Sanitization
Pattern: Sanitize Before Logging
const sanitizeForLogging = (
obj: Obj.PlainObject,
sensitiveFields: string[] = ['password', 'apiKey', 'creditCard', 'ssn']
) =>
pipe(
Obj.flatten(obj, { flattenArrays: true }),
Obj.filterKeys((key) =>
!sensitiveFields.some((sensitive) => key.toLowerCase().includes(sensitive.toLowerCase()))
),
(safe) => Obj.unflatten(safe)
)
// Usage
logger.info('User login', sanitizeForLogging(request))
// Removes every key that contains a sensitive name, at any depthPattern: GDPR-Compliant Data Minimization
const gdprMinimize = {
// Only collect necessary analytics
analytics: (event: UserEvent) =>
pipe(
event,
Obj.mask(['action', 'timestamp', 'category']),
Obj.stripUndefined
// No PII stored
),
// Anonymize user data for support
supportTicket: (user: User) =>
pipe(
user,
Obj.mask(['id', 'createdAt', 'subscription']),
Obj.updatePath(['id'], (id: string) => hashUserId(id))
// Email/name removed, ID hashed
),
// Export user data (GDPR right to access)
exportUserData: (userId: string) =>
fetchAllUserData(userId).then((data) =>
pipe(
Obj.flatten(data),
Obj.filterKeys((key) => !key.includes('_internal')),
(kept) => Obj.unflatten(kept)
)
)
}Pattern: API Response Filtering by Role
const filterByRole = (user: User, role: UserRole) => {
const publicFields = ['id', 'username', 'avatar']
const authenticatedFields = [...publicFields, 'email', 'createdAt']
const adminFields = [...authenticatedFields, 'lastLogin', 'ipAddress', 'internalNotes']
const allowedFields = {
public: publicFields,
user: authenticatedFields,
admin: adminFields
}[role]
return Obj.mask(user, allowedFields)
}Database Operations
Pattern: Query Builder
// A sketch of the data flow. Use parameterized queries with real user input.
const buildWhereClause = (filters: Record<string, unknown>) =>
pipe(
filters,
Obj.compact,
(active) => Obj.flatten(active),
Obj.mapKeys((key) => `table.${key}`),
Obj.mapValues((value) =>
Array.isArray(value) ? `IN (${value.join(',')})` : `= '${value}'`
),
Object.entries,
(entries) => entries.map(([key, value]) => `${key} ${value}`).join(' AND ')
)
// Usage
const sql = `SELECT * FROM users WHERE ${buildWhereClause({
status: 'active',
role: ['admin', 'moderator'],
'profile.verified': true
})}`
// => "SELECT * FROM users WHERE table.status = 'active' AND table.role IN (admin,moderator) AND table.profile.verified = 'true'"Pattern: Prisma-Style Nested Updates
const updateNestedUser = (userId: string, updates: DeepPartial<User>) =>
pipe(
updates,
(partial) => Obj.flatten(partial),
Obj.filterKeys((key) => !key.includes('id')), // Protect ID fields
Obj.stripUndefined,
(flat) =>
Object.entries(flat).reduce(
(query, [path, value]) => query.update({ where: { id: userId }, data: { [path]: value } }),
prisma.user
)
)Complete Real-World Example: Payment Processing
import { isErr, isOk, mapErr, tapErr } from '@ontologyio/receta/result'
// Complete payment processing pipeline
const processPayment = async (stripeWebhook: unknown) => {
// 1. Validate webhook structure
const validated = pipe(
stripeWebhook,
Obj.validateShape(stripeWebhookSchema),
mapErr((error) => ({
code: 'INVALID_WEBHOOK',
message: 'Webhook validation failed',
details: error
}))
)
if (isErr(validated)) {
logger.error('Invalid webhook', validated.error)
return validated
}
// 2. Extract payment data
const paymentData = pipe(
Obj.flatten(validated.value),
Obj.filterKeys((key) =>
['data.object.id', 'data.object.amount', 'data.object.customer'].some((prefix) =>
key.startsWith(prefix)
)
),
Obj.rename({
'data.object.id': 'paymentId',
'data.object.amount': 'amount',
'data.object.customer': 'customerId'
}),
Obj.compact
)
// 3. Update database. flatMap can't await, so the async save sits outside the pipe.
const payment = Obj.validateShape(paymentData, dbPaymentSchema)
const dbResult = isOk(payment) ? await savePaymentToDb(payment.value) : payment
tapErr(dbResult, (error) =>
logger.error('DB save failed', { paymentData: sanitizeForLogging(paymentData), error })
)
// 4. Send notification
if (isOk(dbResult)) {
await sendPaymentConfirmation(dbResult.value.customerId)
}
return dbResult
}Next Steps
- Migration Guide - Migrate from Lodash/Vanilla JS
- API Reference - Quick lookup and cheat sheets
Transformation
Transformation functions allow you to systematically transform or filter object keys and values. These operations are essential for normalizing data, applying bulk transformations, and filtering unwanted properties.
Migration Guide
Step-by-step guide to migrating from Lodash or vanilla JavaScript object operations to Receta's Object module.