DEV Community

Tomas Hornak
Tomas Hornak

Posted on

Understanding the Builder Design Pattern in TypeScript

Design patterns are shared, repeatable solutions for common issues in software engineering. Let's talk about a popular design pattern called the Builder Design Pattern and explore how to implement it in TypeScript with a practical HTTP request builder example.

What is the Builder Pattern?

The Builder Pattern is a design pattern that simplifies the creation of complex objects by providing a step-by-step approach to building different types and representations of an object using a consistent construction process. By separating the construction of an object from its representation, the same construction process can create various representations.

Builder Pattern Concept Code

Let's explore a basic example – a StringBuilder, which constructs a string from words:

class StringBuilder {
  private words: string[] = [];

  addWord(word: string): StringBuilder {
    this.words.push(word);
    return this;
  }

  build(): string {
    return this.words.join(' ');
  }
}

// Client code using the builder
const myStringBuilder = new StringBuilder();
const sentence = myStringBuilder.addWord('Hello').addWord('world!').build();

console.log(sentence); // Outputs: Hello world!
Enter fullscreen mode Exit fullscreen mode

In this code, our builder is the StringBuilder class, containing the addWord and build methods. The addWord method appends a word to the words array. Crucially, it also returns this, the builder object itself. This design facilitates the chaining of method calls, promoting readability and compactness.

The build method completes the process by joining the words together into a sentence. In the client code, we create the StringBuilder, add words to it, and finally use build to get the complete sentence.

Practical Example: HTTP Request Builder

Let's now see how the Builder Pattern can be applied in TypeScript for building an HTTP request.

class HttpRequest {
  method: string;
  url: string;
  body: Record<string, any>;
  headers: HeadersInit;

  constructor() {
    this.body = {};
    this.headers = {};
  }
}

class HttpRequestBuilder {
  httpRequest: HttpRequest;

  constructor() {
    this.httpRequest = new HttpRequest();
  }

  setMethod(method: string): HttpRequestBuilder {
    this.httpRequest.method = method;
    return this;
  }

  setUrl(url: string): HttpRequestBuilder {
    this.httpRequest.url = url;
    return this;
  }

  addBody(key: string, value: any): HttpRequestBuilder {
    this.httpRequest.body[key] = value;
    return this;
  }

  addHeader(name: string, value: string): HttpRequestBuilder {
    this.httpRequest.headers[name] = value;
    return this;
  }

  build(): Request {
    return new Request(this.httpRequest.url, {
      method: this.httpRequest.method,
      headers: this.httpRequest.headers,
      body: JSON.stringify(this.httpRequest.body),
    });
  }
}

let fetchRequest = new HttpRequestBuilder()
  .setMethod('POST')
  .setUrl('https://myapi.com/resource')
  .addHeader('Content-Type', 'application/json')
  .addBody('weather', 'is nice today')
  .build();

console.log(fetchRequest); // Outputs: a `Request` object which can be passed directly to `fetch()`

// Then it can be used using FETCH api:
fetch(fetchRequest)
  .then(response => response.json())
  .then(json => console.log(json))
  .catch(err => console.error('Error:', err));
Enter fullscreen mode Exit fullscreen mode

Within this HTTP request builder, we're applying the Builder Design Pattern to progressively build our request. Notice again the use of this in our builder methods, allowing for the chaining of methods that simplifies the creation process.

Here are some potential benefits and drawbacks of using a Builder Pattern.

Benefits

  1. Flexibility: Constructs various representations of objects.
  2. Reduced Duplication: Mitigates code duplication that arises from numerous constructors.
  3. Easy to Edit: Adjust the object's construction steps without influencing the object's code.

Drawbacks:

  1. Code Complexity: May increase complexity due to new interfaces and classes. (personally, I don't consider this as a drawback)
  2. Limits Validation: The pattern may struggle with property-specific validations such as mutually exclusive properties.

Conclusion

The Builder Pattern helps handle complex or multi-step object construction processes in a more readable and maintainable way. Keep in mind it's just one tool amongst many – its implementation should always consider the specific project requirements and circumstances.

Do you use Builder pattern in your code? If yes, what are your use cases for it?

Top comments (0)