API REST: How to use URL search params to generate a simple query.

Hi! In this article, I would like to share with you an approach that I used to write a library for fetching data using search parameters as query strings.

Application context

First of all, I should describe some aspects of the project environment that motivated me to write this library. The environment for this approach was based on a BFF (Backend for Frontend) architecture. The BFF micro-server is exposed, but the rest of the micro-servers can only be accessed through it. Additionally, only the micro-servers called "core," which is inside the private network, can access the database via a DAO (Data Access Object) design pattern without any ORM (Object-Relational Mapping).

The back-end stack was written using Java, Spring Boot, and a Hexagonal architecture, and the project was deployed over Kubernetes.

URL Query Params Library Overview

With the context set, it's time to show you the proposal to fetch data between the BFF and the Core MS.

First, it was necessary to define a semantic way to fetch data through URLs.

Based on relational database theory, each tuple can be fetched with projection and filter operations. Applying these concepts, it's possible to rewrite a URL like this:

GET host:port/path/to/resource?field=operator(args)
[&__page__=pg(arg1,arg2)&
__columns__=proj(fields)&
__order_by__=ob(asc/desc(fields))]

Where the operator could be, among others: in, eq, gt, gte, lt, lte, btw.

The words wrapped with the decorator '__' indicate reserved words, and the operators proj (projection), pg (page), and ob (order by) are special operators for fetching data.

On the other hand, I tried to define each URL query as a function following the form: var=operator(args).

The following example could fetch all records persisted in a table or file called "great_women," where the name and age comply with the specified conditions. Additionally, it specifies the tuple's projection and order:

https://somehost/great_women?name=in(Heidy;Ada;Marie)&age=gt(20)&" +
        "__columns__=proj(name;age)&" +
        "__order_by__=ob(desc(age))

This approach could be important when using Spring because it's possible to define the controller's query parameters as a Map<String, String>. For this case, this map is:

final Map<String, String> urlQueryParams= Map.of(
        "name", "in(Heidy;Ada;Marie)",
        "age", "gt(20)",
        "__columns__", "proj(name;age)",
        "__order_by__", "ob(desc(age))"
);

Second, I defined an agnostic data structure to describe a query:

@Value
@Builder
public class Query {
    String resource;
    Set<String> columns;
    Set<Filter> filters;
    Set<OrderBy> orderBy;
    Page page;
}

Third, I wrote an interpreter for this data structure to the target query dialect, resulting in a structure like this:

@Value
@Builder
public class SQLQuery {
    String query;
    Map<String, Object> arguments;
}

Where the string query assumes the form:

SQL_QUERY = "SELECT age, name" +
        " FROM dbo.great_women" +
        " WHERE age > :AGE AND" +
        " name IN (:NAME)" +
        " ORDER BY age DESC" +
        " OFFSET :OFFSET ROWS FETCH NEXT :SIZE ROWS ONLY"

Note that this query does not concatenate values but specifies the query with its corresponding variables. The values of these variables are provided alongside the string query as a Map<String, Object>, and it's an important aspect to avoid SQL injections.

Finally, a test fragment for this example is:

URL Query Params Given from Controller (Mapping with Spring Boot)

final Map<String, String> urlQueryParams= Map.of(
        "name", "in(Heidy;Ada;Marie)",
        "age", "gt(20)",
        "__columns__", "proj(name;age)",
        "__order_by__", "ob(desc(age))"
);

Translate it to a Query intermediate data structure:

final Query query = UrlQueryParamsInterpreter.builder("great_women",fieldParsers)
        .translate(urlQueryParams);

Finally, translate this Query data structure to the target SQL dialect:

final SQLQuery sqlQuery = SqlServerInterpreter.builder("dbo", Map.of()).translate(query);

Where the String query expected is like this:

/The SQL query string expected is:
final static String SQL_QUERY = "SELECT age, name" +
        " FROM dbo.great_women" +
        " WHERE age > :AGE AND" +
        " name IN (:NAME)" +
        " ORDER BY age DESC" +
        " OFFSET :OFFSET ROWS FETCH NEXT :SIZE ROWS ONLY";

And its values:

final Map<String, Object> params = Map.of(
        "AGE", 20,
        "NAME", new Object[]{"Heidy", "Ada", "Marie"},
        "OFFSET", 0,
        "SIZE", 10
);

Finally considerations

Thank you very much for reaching this part of the article!

This library was rewritten based on a real case. It is still in development, and I don't recommend using it in production yet. I hope to publish a release candidate of this library soon. If you found it interesting, let me know.

Resources

https://github.com/rfjuarez/url-query-params

About Me

I'm an information systems engineer and passionate programmer. I hope you enjoy this article.