Skip to content

jakubjanczyk/object-builder

Repository files navigation

object-builder

Lightweight, zero-dependency object builder for tests and production use cases.

Break free from boilerplate and build expressive objects in seconds. Compose fluent custom builders that capture business language, keep teams aligned, and leave room for playful experimentation.

object-builder pairs auto-generated with... helpers with your own custom builders, so complex updates stay type-safe without sacrificing readability—whether you're shaping fixtures, seeds, or production data flows. It works seamlessly in TypeScript and plain JavaScript projects alike.

Why builders help

  • ✅ Fluent API keeps data tweaks in a single expression.
  • ✅ Custom builders unlock richer scenarios.
  • ✅ Defaults live in one place; every property stays valid until you change it.
  • ✅ Callers do not need to remember the shape of nested objects.
  • ✅ TypeScript surfaces breaking changes immediately, so maintenance stays simple.
  • ✅ No more forgotten return this; every builder call is chainable.
  • ✅ It's immutable - your original objects stays unchanged and every built object is independent.
  • ✅ It's composable - use multiple builders for nested objects.
  • ✅ Powerful helper for tests - define defaults and only change what matters for give test case.

Installation

bun add @jjanczyk/object-builder
# or
pnpm add @jjanczyk/object-builder
# or
npm install @jjanczyk/object-builder
# or
yarn add @jjanczyk/object-builder

Quick start

These snippets show the main patterns you'll reach for every day.

Defaults plus custom builders

Start with a baseline object and layer on fluent helpers that express domain language.

import builder from '@jjanczyk/object-builder'

type Message = {
  id: string
  name: string
  type: 'simple' | 'rich'
  metadata?: Record<string, string>
}

const aMessage = () => builder<Message>(
  {
    id: 'abc',
    name: 'Test Message',
    type: 'simple',
  },
  (message) => ({
    withPriorityAlert(priority: 'high' | 'low', source: string) {
      message.type = 'rich'
      message.metadata = {
        ...(message.metadata ?? {}),
        severity: priority,
        source,
      }
    },
    withMetadataPair(key: string, value: string) {
      message.metadata = { ...(message.metadata ?? {}), [key]: value }
    },
  })
)

const scheduledMessage = aMessage()
  .withId('abc')
  .withName('Alert')
  .withPriorityAlert('high', 'monitoring-service')
  .withMetadataPair('ticket', 'INC-123')
  .build()

// Result:
// { id: 'abc', name: 'Alert', type: 'rich', metadata: { severity: 'high', source: 'monitoring-service', ticket: 'INC-123' } }

The generated with... methods cover simple assignments, while your bespoke helpers coordinate multi-field changes like alert priorities and metadata.

Defaults without custom builders

Need only the auto-generated mutators? Provide defaults and use the fluent API as-is.

import builder from '@jjanczyk/object-builder'

const todoBuilder = builder({
  id: 'todo-1',
  title: 'Write docs',
  done: false,
})

const urgentTodo = todoBuilder
  .withTitle('Ship release notes')
  .withDone(true)
  .build()

// Result:
// { id: 'todo-1', title: 'Ship release notes', done: true }

Every property on the initial object unlocks a corresponding with... helper for quick tweaks without custom code.

Nested builders

Have some nested objects that needs to be built? Simply use multiple builders!

import {builder} from "@jjanczyk/builder";

type Message = {
    id: string
    text: string
}

type Chat = {
    id: string
    title: string
    messages: Message[]
}

const aMessage = () => builder<Message>({id: '1', text: 'example'})

const aChat = () => builder<Chat>({id: '1', title: 'example', messages: []})

const chat = aChat()
    .withTitle('My chat')
    .withMessages([
        aMessage()
            .withText('My message')
            .build()
    ])
    .build()

// Result:
// { id: '1', title: 'My chat', messages: [{ id: '1', text: 'My message' }] }

Standalone builder

You can skip the initial object entirely and fill everything through the fluent API.

import builder from '@jjanczyk/object-builder'

type Account = {
  id: string
  email: string
  role?: 'user' | 'admin'
}

const account = builder<Account>()
  .withId('user-123')
  .withEmail('user@example.com')
  .withRole('admin')
  .build()

// Result:
// { id: 'user-123', email: 'user@example.com', role: 'admin' }

When you omit initialData, or pass incomplete data, TypeScript will hide .build() until all required fields are set. In JavaScript, just make sure you set the required fields before calling .build() so the result includes everything you expect.

Cloning multiple instances

Need several copies of the same setup—for example, to seed arrays or prepare fixtures? Use buildList(count) to produce any number of independent objects.

import builder from '@jjanczyk/object-builder'

const userBuilder = builder({
  id: 'user-1',
  email: 'user@example.com',
  active: true,
})

const threeUsers = userBuilder.withActive(false).buildList(3)
// each user has unique object references:
threeUsers[0] !== threeUsers[1] // true

// Result:
// [
//   { id: 'user-1', email: 'user@example.com', active: false },
//   { id: 'user-1', email: 'user@example.com', active: false },
//   { id: 'user-1', email: 'user@example.com', active: false },
// ]

Need to tweak each item? Pass an optional mapper that receives the fresh clone and index.

import builder from '@jjanczyk/object-builder'

const userBuilder = builder({
    id: 'user-1',
    email: 'user@example.com',
    active: true,
})

const customizedUsers = userBuilder.buildList(3, (user, index) => {
  user.id = `user-${index + 1}`
  user.email = `user-${index + 1}@example.com`
  user.active = index === 0
})
// each user has unique object references:
customizedUsers[0] !== customizedUsers[1] // true
customizedUsers[0].id // "user-1"

// Result:
// [
//   { id: 'user-1', email: 'user-1@example.com', active: true },
//   { id: 'user-2', email: 'user-2@example.com', active: false },
//   { id: 'user-3', email: 'user-3@example.com', active: false },
// ]

API at a glance

  • builder(initialData?, customBuilders?) deep-copies initialData (when provided) and returns a fluent proxy.
  • .with<Property>(value) is generated for every key in initialData.
  • .from(otherObject) resets the builder to another object (copied to keep data isolated).
  • .build() returns the accumulated object.
  • .buildList(count, customizer?) returns count independent copies of the current builder state and lets you tweak each item via mapFn(item, index).
  • builder<Type>() lets you start from scratch and add properties via fluent calls.
  • customBuilders(target) lets you define additional fluent helpers. They receive the mutable target so you can coordinate multiple fields and validations.
  • Works in JavaScript: import builder and use the same fluent API; the proxy enforces with... naming so the ergonomics match your TypeScript usage.

Because everything runs through a Proxy, every helper returns the builder automatically—no need to remember return this. TypeScript infers the allowed methods from the initial data and your custom helpers, so misuse is caught at compile time. Custom helpers that coordinate several fields (like withPriorityAlert) make the builder especially powerful for non-trivial scenarios.

Extending the builder

  • Add new fields to initialData to expose more with... methods.
  • Keep custom builder helpers idempotent or document side effects—they execute directly against the target object.
  • Validate inputs inside custom builders before mutating the target to avoid leaking invalid state.
  • Share reusable defaults by exporting factory functions (export const aMessage = () => builder(...)) and reusing the fluent helpers wherever you need them.

Development

This package was developed with bun

  • bun install installs dependencies
  • bun test runs tests
  • bun lint runs lint check
  • bun fix fixes lint/format issues

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published