Introduction
The Builder pattern constructs complex objects step by step. Instead of a constructor with many parameters, you chain method calls to configure the object. This makes code readable and prevents errors from parameter ordering.
Key Concepts
Builder: Object that accumulates configuration through method chaining.
Fluent Interface: Methods return this for chaining.
Director: Optional class that defines build steps order.
Real World Context
Query builders (Knex), test data builders, configuration objects, HTTP request builders—builders appear throughout JavaScript libraries.
Deep Dive
Basic Builder
This builder accumulates user properties step by step, with each setter returning this to enable fluent chaining:
javascriptclass UserBuilder { #user = {}; setName(name) { this.#user.name = name; return this; // Enable chaining } setEmail(email) { this.#user.email = email; return this; } setRole(role) { this.#user.role = role; return this; } setPermissions(...permissions) { this.#user.permissions = permissions; return this; } build() { // Validation if (!this.#user.name || !this.#user.email) { throw new Error('Name and email required'); } return { ...this.#user }; } } const user = new UserBuilder() .setName('Alice') .setEmail('alice@example.com') .setRole('admin') .setPermissions('read', 'write') .build();
The build() method validates required fields and returns a frozen copy of the accumulated data, ensuring the builder cannot produce an invalid object.
Query Builder Example
Query builders are one of the most common real-world uses of this pattern. Each method appends a clause to the SQL query being constructed:
javascriptclass QueryBuilder { #query = { select: '*', from: '', where: [], orderBy: null, limit: null }; select(...fields) { this.#query.select = fields.length ? fields.join(', ') : '*'; return this; } from(table) { this.#query.from = table; return this; } where(condition) { this.#query.where.push(condition); return this; } orderBy(field, direction = 'ASC') { this.#query.orderBy = `${field} ${direction}`; return this; } limit(n) { this.#query.limit = n; return this; } build() { let sql = `SELECT ${this.#query.select} FROM ${this.#query.from}`; if (this.#query.where.length) { sql += ` WHERE ${this.#query.where.join(' AND ')}`; } if (this.#query.orderBy) sql += ` ORDER BY ${this.#query.orderBy}`; if (this.#query.limit) sql += ` LIMIT ${this.#query.limit}`; return sql; } } const query = new QueryBuilder() .select('id', 'name', 'email') .from('users') .where('active = true') .where('age > 18') .orderBy('name') .limit(10) .build();
Notice how multiple .where() calls accumulate conditions joined by AND. The builder makes complex query construction readable and safe.
Functional Builder
You can also build without classes using closures. Note: use regular methods (shorthand syntax) so that this refers to the returned object, enabling chaining.
javascriptconst createRequest = () => { const config = { method: 'GET', headers: {} }; const builder = { method(m) { config.method = m; return builder; }, header(k, v) { config.headers[k] = v; return builder; }, body(b) { config.body = b; return builder; }, build() { return { ...config }; } }; return builder; };
Each method returns the builder object explicitly rather than this, since arrow functions do not have their own this binding. By returning the builder reference explicitly, we avoid the this pitfall of arrow functions (which don't have their own this).
Common Pitfalls
- Mutable state issues: Reset or clone between builds.
- Too many methods: Builder becomes unwieldy.
- Missing validation: build() should validate.
Best Practices
- Always return this: For fluent chaining.
- Validate in build(): Catch errors early.
- Reset state after build: Or create new builder per object.
- Use for 3+ parameters: Simpler cases don't need builders.
Summary
Builders construct complex objects step by step through method chaining. Always return this for fluent interface. Validate in build(). Use for objects with many configuration options.
Code Examples
class QueryBuilder {
#table = '';
#conditions = [];
#orderBy = '';
from(table) { this.#table = table; return this; }
where(condition) { this.#conditions.push(condition); return this; }
order(field) { this.#orderBy = field; return this; }
build() {
let query = `SELECT * FROM ${this.#table}`;
if (this.#conditions.length) query += ` WHERE ${this.#conditions.join(' AND ')}`;
if (this.#orderBy) query += ` ORDER BY ${this.#orderBy}`;
return query;
}
}
const sql = new QueryBuilder()
.from('users')
.where('age > 18')
.where('active = true')
.order('name')
.build();
// "SELECT * FROM users WHERE age > 18 AND active = true ORDER BY name"