Crafting Query Strings with the Builder Pattern in Typescript

Refactoring Query String Generation with the Builder Pattern

Suyeon Kang
suyeonme
3 min readApr 27, 2024

--

Photo by Pawel Czerwinski on Unsplash

In web development, the process of generating query strings is a common task, especially when working with APIs. Query strings are crucial for passing data between different pages or between the client and server.

In this post, we’ll explore a flexible approach to constructing query strings using the Builder pattern in Typescript. By leveraging this design pattern, we can create query strings dynamically with ease, enhancing the maintainability and readability of our codebase. Let’s dive into the details of how the Builder pattern can streamline the process of query string generation.

Analyzing the following example code

Recently, I stumbled upon the following code snippet. GenerateQueryString has successfully done its job, configuring a query string dynamically with the parameters.

const generateQueryString = ({ filterKey, filterValue, startTime, endTime}: Partial<{
filterKey: string;
filterValue: string;
startTime: number;
endTime: number;
}>): string => {
return `filterKey=${filterKey ? filterKey : 'hash'}${filterValue ? `&filterValue=${encodeURI(filterValue)}` : ''}
${startTime ? `&stime=${startTime}` : ''}${endTime ? `&etime=${endTime}` : ''}`;
};

const queryStr = generateQueryString({ ilterKey, filterValue, startTime, endTime });
...

However, the implementation lacks both reusability and readability.

Conditional Logic decreases the readability

The code uses conditional ternary operators to construct the query string. This approach makes the code harder to read and maintain, especially as the number of parameters increases.

${startTime ? `&stime=${startTime}` : ''}

Lack of Separation of Concerns

The function is responsible for both constructing the query string and encoding values. This violates the principle of single responsibility and can lead to less modular and harder-to-maintain code.

${filterValue ? `&filterValue=${encodeURI(filterValue)}` : ''}

Limited Flexibility

The function signature expects specific parameters, limiting its flexibility and making it less reusable in scenarios where different sets of query parameters are needed.

Refactor the code using the Builder pattern

What is a Builder pattern?

The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation. It allows the construction process to be step-by-step, enabling the creation of different variations of an object using the same construction code. This pattern promotes code reusability, readability, and flexibility by providing a clear, fluent interface for building objects with varying configurations.

Create a QueryStringBuilder Class

Implement a class that serves as a builder for constructing query strings. QueryStringBuilder class is created by applying the Builder pattern so that provides methods for setting individual query parameters.

class QueryStringBuilder {
private baseUrl: string;
private queryString: string;

constructor(baseUrl: string) {
this.baseUrl = baseUrl;
this.queryString = '';
}

addQueryString(key: string, value: string): void {
if (this.queryString !== '') {
this.queryString += '&';
}
this.queryString += `${key}=${encodeURIComponent(value)}`;
}

build(): string {
return this.baseUrl + '?' + this.queryString;
}
}

Implementation

Utilizing the Builder pattern enables the dynamic addition of query parameters while also effectively separating concerns.

const urlBuilder = new QueryStringBuilder('https://example.com');

if(filterKey) urlBuilder.addQueryString('filterKey', filterKey ?? 'hash');
if(filterValue) urlBuilder.addQueryString('filterValue', encodeURI(filterValue));
if(startTime) urlBuilder.addQueryString('startTime', startTime);
if(endTime) urlBuilder.addQueryString('startTime', endTime);

const resultUrl = urlBuilder.build();
return resultUrl;

Conclusion

Implementing the Builder pattern for query string construction in Web development significantly improves code quality by enhancing reusability, readability, and flexibility.

If you are interested in other design patterns, I recommend reading Design Patterns: Elements of Reusable Object-Oriented Software.

Happy coding!

--

--