You Don’t Need GraphQL

GraphQL is a technology that seems to be getting a lot of attention in the developer community at the moment. Advocates describe it as "a better REST", claming that it offers several advantages over traditional REST APIs:

  • Single request with nested results vs. multiple separate requests
  • Single endpoint for all requests vs. one endpoint per resource
  • Single evolving version vs. multiple (presumably incompatible) versions

For example, the following GraphQL query might be used to retrieve an employee record from a hypothetical service based on the MySQL "employees" sample database. In addition to the employee number, first name, and last name, the query also requests the employee's title and salary history:

{
  employee(id: 10004) {
    employeeNumber
    firstName
    lastName
    titles {
      title
      fromDate
      toDate
    }
    salaries {
      salary
      fromDate
      toDate
    }
  }
}

The response might look something like this, with some results omitted for brevity:

{
  "employeeNumber": 10004,
  "firstName": "Chirstian",
  "lastName": "Koblick",
  "titles": [
    {
      "title": "Senior Engineer",
      "fromDate": 817794000000,
      "toDate": 253370782800000
    },
    ...
  ],
  "salaries": [
    {
      "salary": 74057,
      "fromDate": 1006837200000,
      "toDate": 253370782800000
    },
    ...
  ]
}

A RESTful Implementation

The data model for the sample database is shown below:

Employees Sample Database

A typical REST API might provide access to employee, title, and salary resources as follows:

GET /employees/10004
{
  "employeeNumber": 10004,
  "firstName": "Chirstian",
  "lastName": "Koblick"
}
GET /employees/10004/titles
[
  {
    "title": "Senior Engineer",
    "fromDate": 817794000000,
    "toDate": 253370782800000
  },
  ...
]
GET /employees/10004/salaries
[
  {
    "salary": 74057,
    "fromDate": 1006837200000,
    "toDate": 253370782800000
  },
  ...
]

This is indeed more verbose than the GraphQL version. However, there is nothing preventing a REST API from providing a similar interface.

For example, the following service method (implemented using the open-source HTTP-RPC framework) returns the same information as the GraphQL query. As with the GraphQL version, all of the data is obtained with a single request:

@RequestMethod("GET")
@ResourcePath("?:employeeNumber")
public void getEmployee(List<String> details) throws SQLException, IOException {
    String employeeNumber = getKey("employeeNumber");

    Parameters parameters = Parameters.parse("SELECT emp_no AS employeeNumber, "
        + "first_name AS firstName, "
        + "last_name AS lastName "
        + "FROM employees WHERE emp_no = :employeeNumber");

    parameters.put("employeeNumber", employeeNumber);

    try (Connection connection = DriverManager.getConnection(DB_URL);
        PreparedStatement statement = connection.prepareStatement(parameters.getSQL())) {
        parameters.apply(statement);

        try (ResultSet resultSet = statement.executeQuery()) {
            ResultSetAdapter resultSetAdapter = new ResultSetAdapter(resultSet);

            for (String detail : details) {
                switch (detail) {
                    case "titles": {
                        resultSetAdapter.attach("titles", "SELECT title, "
                            + "from_date AS fromDate, "
                            + "to_date as toDate "
                            + "FROM titles WHERE emp_no = :employeeNumber");

                        break;
                    }

                    case "salaries": {
                        resultSetAdapter.attach("salaries", "SELECT salary, "
                            + "from_date AS fromDate, "
                            + "to_date as toDate "
                            + "FROM salaries WHERE emp_no = :employeeNumber");

                        break;
                    }
                }
            }

            getResponse().setContentType("application/json");

            JSONEncoder jsonEncoder = new JSONEncoder();

            jsonEncoder.writeValue(resultSetAdapter.next(), getResponse().getOutputStream());
        }
    } finally {
        getResponse().flushBuffer();
    }
}

The initial query retreives the employee's number, first name, and last name from the "employees" table. Subqueries to return the employee's salary and title history are optionally attached based on the values provided in the details parameter. Column aliases are used in all of the queries to make the field names more JSON-friendly.

Callers can access the API via a standard HTTP GET request, as shown below:

GET /employees/10004?details=titles&details=salaries
{
  "employeeNumber": 10004,
  "firstName": "Chirstian",
  "lastName": "Koblick",
  "titles": [
    {
      "title": "Senior Engineer",
      "fromDate": 817794000000,
      "toDate": 253370782800000
    },
    ...
  ],
  "salaries": [
    {
      "salary": 74057,
      "fromDate": 1006837200000,
      "toDate": 253370782800000
    },
    ...
  ]
}

Additional Observations

GraphQL advocates tout its single-endpoint model as a major advantage over REST. This capability is not exclusive to GraphQL – it is certainly possible for REST APIs to be implemented using a single endpoint as well. However, such a service would probably become untenable very quickly. A collection of independent endpoints, each of which represent a specific resource or set of resources, will most likely be much more manageable in the long run.

Further, the concept of a single evolving version is not unique to GraphQL. Implementing a successful versioning strategy is difficult, and there are many ways of approaching it. However, there is nothing to preclude a REST service from providing backwards compatibility. It is simply one option among many.

Finally, adopting GraphQL requires services to be completely re-implemented using the GraphQL library. For any non-trivial application, this would most likely be a major undertaking. Additionally, it forces clients to use GraphQL as well, rather than standard HTTP operations such as GET and POST. This means that GraphQL APIs also can't be tested as easily in a web browser or using command-line utilties such as curl.

So, while there are certainly a number of compelling reasons to consider GraphQL, you don't actually need to use GraphQL to take advantage of them.

For more information on HTTP-RPC, see the project README.

HTTP-RPC 5.8 Released

HTTP-RPC is an open-source framework for implementing and interacting with RESTful and REST-like web services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is distributed as a single JAR file that is about 66KB in size, making it an ideal choice for applications where a minimal footprint is desired.

HTTP-RPC version 5.8 is now available for download and via Maven Central. This release adds support for automatically generated API documentation. For example, given the following service implementation:

@WebServlet(urlPatterns={"/math/*"})
public class MathService extends WebService {
    @RequestMethod("GET")
    @ResourcePath("sum")
    public double getSum(double a, double b) {
        return a + b;
    }
    
    @RequestMethod("GET")
    @ResourcePath("sum")
    public double getSum(List<Double> values) {
        double total = 0;
    
        for (double value : values) {
            total += value;
        }
    
        return total;
    }
}

a GET request for /math?api will now return a document that describes all of the service's endpoints and associated operations:

/math/sum

GET (a: double, b: double) -> double
GET (values: [double]) -> double

Services can additionally provide localized documentation for each method by including one or more resource bundles on the classpath. For example, the following MathService.properties file could be used to provide localized method descriptions for the MathService class:

MathService = Math example service.
getSum = Calculates the sum of two or more numbers.
getSum.a = The first number.
getSum.b = The second number.
getSum.values = The numbers to add.

The first line describes the service itself. The remaining lines describe the service methods and their parameters. A localized description of the math service might look like this:

Math example service.

/math/sum

GET (a: double, b: double) -> double

Calculates the sum of two or more numbers.

  • a The first number.
  • b The second number.
GET (values: [double]) -> double

Calculates the sum of two or more numbers.

  • values The numbers to add.

This allows consumers in any locale to easily discover and understand the operations supported by an endpoint.

For more information, see the project README.

HTTP-RPC 5.7 Released

HTTP-RPC version 5.7 is now available for download and via Maven Central. Among other enhancements, this release adds support for consuming REST services in JavaScript via the Nashorn scripting engine.

In a previous update, I showed how HTTP-RPC’s WebServiceProxy class can be used to access the operations of a simple math service:

// GET /math/sum?a=2&b=4
WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/math/sum"));

HashMap arguments = new HashMap();

arguments.put("a", 4);
arguments.put("b", 2);

webServiceProxy.setArguments(arguments);

System.out.println(webServiceProxy.invoke()); // 6.0
// GET /math/sum?values=1&values=2&values=3
WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/math/sum"));

HashMap arguments = new HashMap();

arguments.put("values", Arrays.asList(1, 2, 3));

webServiceProxy.setArguments(arguments);

System.out.println(webServiceProxy.invoke()); // 6.0

The following example demonstrates how the same operations can be invoked from JavaScript:

var webServiceProxy = new org.httprpc.WebServiceProxy("GET", new java.net.URL("http://localhost:8080/httprpc-test/math/sum"));

// GET /math/sum?a=2&b=4
webServiceProxy.arguments = {"a": 4, "b": 2};

print(webServiceProxy.invoke()); // 6

// GET /math/sum?values=1&values=2&values=3
webServiceProxy.arguments = {"values": Java.asJSONCompatible([1, 2, 3])};

print(webServiceProxy.invoke()); // 6

Note that, because Nashorn automatically translates Java List and Map values to JavaScript array and object instances respectively, transformation or adaptation of service responses is not generally necessary. For example, the following script would print the third value in the Fibonacci sequence:

var webServiceProxy = new org.httprpc.WebServiceProxy("GET", new java.net.URL("http://localhost:8080/httprpc-test/test/fibonacci"));

// [1, 2, 3, 5, 8, 13]
var fibonacci = webServiceProxy.invoke();

print(fibonacci[2]); // 3

For more information, see the project README.

Efficiently Producing and Consuming CSV in Java using HTTP-RPC

In a previous article, I discussed how the open-source HTTP-RPC framework can be used to efficiently transform JDBC query results into JSON. However, while JSON is an extremely common and well-supported data format, it may in some cases be preferable to return a CSV document instead. Because field keys are specified only at the beginning of a CSV document rather than being duplicated for every record, CSV generally requires less bandwidth than JSON. Additionally, consumers can begin processing CSV as soon as the first record arrives, rather than waiting for the entire document to download.

The previous article demonstrated how HTTP-RPC's ResultSetAdapter and JSONEncoder classes could be used to easily generate a JSON response from a SQL query on this table, taken from the MySQL sample database:

CREATE TABLE pet (
    name VARCHAR(20),
    owner VARCHAR(20),
    species VARCHAR(20), 
    sex CHAR(1), 
    birth DATE, 
    death DATE
);

The following service method returns the same results encoded as CSV. The only difference in this version is the use of HTTP-RPC's CSVEncoder class instead of JSONEncoder:

@RequestMethod("GET")
public void getPets(String owner) throws SQLException, IOException {
    try (Connection connection = DriverManager.getConnection(DB_URL)) {
        Parameters parameters = Parameters.parse("SELECT name, species, sex, birth FROM pet WHERE owner = :owner");

        parameters.put("owner", owner);

        try (PreparedStatement statement = connection.prepareStatement(parameters.getSQL())) {
            parameters.apply(statement);

            try (ResultSet resultSet = statement.executeQuery()) {
                CSVEncoder csvEncoder = new CSVEncoder();
                
                csvEncoder.writeValue(new ResultSetAdapter(resultSet), getResponse().getOutputStream());
            }
        }
    } finally {
        getResponse().flushBuffer();
    }
}

The response might look something like this, where each record in the document represents a row from the result set (dates are represented using epoch time):

"name","species","sex","birth"
"Claws","cat","m",763880400000
"Chirpy","bird","f",905486400000
"Whistler","bird",,881643600000

Consuming a CSV Response

On the other end, HTTP-RPC's CSVDecoder class can be used to efficiently process the contents of a CSV document. Rather than reading the entire payload into memory and returning the data as a random-access list of map values, CSVDecoder returns a cursor over the records in the document. This allows a client to read records essentially as they are being produced, reducing memory consumption and improving throughput.

For example, the following code could be used to consume the response generated by the web service:

WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/pets"));

webServiceProxy.getArguments().put("owner", "Gwen");
webServiceProxy.getArguments().put("format", "csv");

webServiceProxy.invoke((inputStream, contentType) -> {
    CSVDecoder csvDecoder = new CSVDecoder();

    CSVDecoder.Cursor pets = csvDecoder.readValues(inputStream);

    for (Map<String, String> pet : pets) {
        System.out.println(String.format("%s is a %s", pet.get("name"), pet.get("species")));
    }

    return null;
});

This code produces the following output:

Claws is a cat
Chirpy is a bird
Whistler is a bird

Additionally, the adapt() method of the CSVDecoder.Cursor class can be used to facilitate typed iteration of CSV data. This method produces an Iterable sequence of values of a given type representing the rows in the document. The returned adapter uses dynamic proxy invocation to map properties declared by the interface to map values in the cursor. A single proxy instance is used for all rows to minimize heap allocation.

For example, the following interface might be used to model the pet records shown above:

public interface Pet {
    public String getName();
    public String getOwner();
    public String getSpecies();
    public String getSex();
    public Date getBirth();
}

This code uses adapt() to create an iterable sequence of Pet values from the CSV response:

CSVDecoder csvDecoder = new CSVDecoder();

CSVDecoder.Cursor pets = csvDecoder.readValues(inputStream);

for (Pet pet : pets.adapt(Pet.class)) {
    System.out.println(String.format("%s is a %s", pet.getName(), pet.getSpecies()));
}

The output is identical to the previous example; however, in this case, the fields of each record are retrieved in a type-safe manner (a feature whose value becomes much more obvious when handling CSV documents containing numeric or boolean values, for example).

More Information

This article provided an example of how HTTP-RPC's CSVEncoder and CSVDecoder classes can be used to efficiently stream and process JDBC query results as CSV. For more information, see the project README.

HTTP-RPC 5.5 Released

HTTP-RPC is an open-source framework for implementing REST services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is distributed as a single JAR file that is about 52KB in size, making it an ideal choice for applications such as microservices where a minimal footprint is desired.

HTTP-RPC version 5.5 is now available for download and via Maven Central. The primary enhancement in this release is the new WebServiceProxy class, which allows an HTTP-RPC web service to act as a consumer of other REST services.

Service proxies are initialized via a constructor that takes the following arguments:

  • method – the HTTP method to execute
  • url – an instance of java.net.URL representing the target of the operation

Request headers and arguments are specified via the proxy's getHeaders() and getArguments() methods, respectively. Like HTML forms, arguments are submitted either via the query string or in the request body. As with HTML, POST requests may be submitted as either "application/x-www-form-urlencoded" or "multipart/form-data" (specified via the proxy's setEncoding() method). Custom request encodings are also supported.

Service operations are invoked via one of the following methods:

public <T> T invoke() throws IOException { ... }
public <T> T invoke(ResponseHandler<T> responseHandler) throws IOException { ... }

The first version automatically deserializes a successful response using HTTP-RPC's JSONDecoder class. The second version allows a caller to perform custom deserialization of the server response.

For example, the following code snippets demonstrate how WebServiceProxy might be used to access the operations of a simple math service:

// GET /math/sum?a=2&b=4
WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/math/sum"));

webServiceProxy.getArguments().put("a", 4);
webServiceProxy.getArguments().put("b", 2);

Number result = webServiceProxy.invoke();

System.out.println(result); // 6.0
// GET /math/sum?values=1&values=2&values=3
WebServiceProxy webServiceProxy = new WebServiceProxy("GET", new URL("http://localhost:8080/httprpc-test/math/sum"));

webServiceProxy.getArguments().put("values", Arrays.asList(1, 2, 3));

Number result = webServiceProxy.invoke();

System.out.println(result); // 6.0

Typed Web Service Access

Additionally, the adapt() methods of the WebServiceProxy class can be used to facilitate type-safe access to web services:

public static <T> T adapt(URL baseURL, Class<T> type) { ... }
public static <T> T adapt(URL baseURL, Class<T> type, Map<String, ?> headers) { ... }

Both versions take a base URL and an interface type as arguments and return an instance of the given type that can be used to invoke service operations. The second version also accepts a map of header values that will be submitted with every service request.

The RequestMethod annotation is used to associate an HTTP verb with an interface method. The optional ResourcePath annotation can be used to associate the method with a specific path relative to the base URL. If unspecified, the method is associated with the base URL itself.

For example, the following interface might be used to model the operations of the example math service:

public interface MathService {
    @RequestMethod("GET")
    @ResourcePath("sum")
    public Number getSum(double a, double b) throws IOException;

    @RequestMethod("GET")
    @ResourcePath("sum")
    public Number getSum(List<Double> values) throws IOException;
}

This code snippet uses the adapt() method to create an instance of MathService, then invokes the getSum() methods on the returned instance. The results are identical to the previous example:

MathService mathService = WebServiceProxy.adapt(new URL("http://localhost:8080/httprpc-test/math/"), MathService.class);

// GET /math/sum?a=2&b=4
mathService.getSum(4, 2); // 6.0

// GET /math/sum?values=1&values=2&values=3
mathService.getSum(Arrays.asList(1.0, 2.0, 3.0)); // 6.0

Summary

This article introduced HTTP-RPC's new WebServiceProxy class, which allows an HTTP-RPC service to act as a consumer of other REST services. For more information, see the project README.

HTTP-RPC 5.3 Released

HTTP-RPC 5.3 is now available for download. This release adds support for typed result set iteration.

As previously discussed, HTTP-RPC's ResultSetAdapter class can be used to optimize transformation of JDBC result set data to JSON. The new adapt() method the 5.3 release adds to ResultSetAdapter can be used to facilitate type-safe iteration of query results. This method takes a ResultSet and an interface type as arguments, and returns an Iterable of the given type representing the rows in the result set.

For example, this interface might be used to model the results of a query on the "pet" table from the MySQL sample database:

public interface Pet {
    public String getName();
    public String getOwner();
    public String getSpecies();
    public String getSex();
    public Date getBirth();
}

The following service method uses adapt() to create an iterable sequence of Pet values. It wraps the adapter's iterator in a stream, and then uses the stream to calculate the average age of all pets in the database. The getBirth() method declared by the Pet interface is used to retrieve each pet's age in epoch time. The average value is converted to years at the end of the method:

@RequestMethod("GET")
@ResourcePath("/average-age")
public double getAverageAge() throws SQLException {
    Date now = new Date();

    double averageAge;
    try (Connection connection = DriverManager.getConnection(DB_URL);
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("SELECT birth FROM pet")) {
        Iterable<Pet> pets = ResultSetAdapter.adapt(resultSet, Pet.class);

        Stream<Pet> stream = StreamSupport.stream(pets.spliterator(), false);

        averageAge = stream.mapToLong(pet -> now.getTime() - pet.getBirth().getTime()).average().getAsDouble();
    }

    return averageAge / (365.0 * 24.0 * 60.0 * 60.0 * 1000.0);
}

For more information, see the project README.

Efficiently Transforming JDBC Query Results to JSON

NOTE The DispatcherServlet class mentioned below has been renamed to WebService since this article was originally written.

A lot of enterprise data is stored in relational databases and accessed via SQL queries. Many web services are little more than HTTP-based wrappers around such queries.

Unfortunately, transforming query results to JSON so it can be consumed by a client application often involves numerous inefficient steps, such as binding each row to a data object and loading the entire data set into memory before serializing it back to the caller. This type of approach has a negative impact on performance and scalabilty. Each row requires multiple heap allocations and constructor invocations, increasing latency and CPU load. Worse, the caller does not receive a response until the entire data set has been processed.

Further, since each response is loaded entirely into memory, high-volume applications require a large amount of RAM, and can only scale through the addition of more physical hardware. Eventually, the garbage collector has to run, slowing down the entire system.

A much more efficient approach is to stream response data. Instead of copying the query results into an in-memory data structure before sending the response, the web service can write a row of data to the output stream each time a row is read from the result set. This allows a client to begin receiving the data as soon as it is available, significantly reducing latency. Also, because no intermediate data structures are created, CPU and memory load is reduced, allowing each server to handle a higher number of concurrent requests. Finally, because fewer heap allocations are required, the garbage collector needs to run much less frequently, resulting in fewer system pauses.

Introducing HTTP-RPC

HTTP-RPC is an open-source framework for implementing REST services in Java. It is extremely lightweight and requires only a Java runtime environment and a servlet container. The entire framework is distributed as a single JAR file that is less than 30KB in size, making it an ideal choice for applications such as microservices where a minimal footprint is desired.

DispatcherServlet

HTTP-RPC’s DispatcherServlet type provides an abstract base class for REST-based web services. Service operations are defined by adding public methods to a concrete service implementation.

Methods are invoked by submitting an HTTP request for a path associated with a servlet instance. Arguments are provided either via the query string or in the request body, like an HTML form. DispatcherServlet converts the request parameters to the expected argument types, invokes the method, and writes the return value to the output stream as JSON.

The RequestMethod annotation is used to associate a service method with an HTTP verb such as GET or POST. The optional ResourcePath annotation can be used to associate the method with a specific path relative to the servlet. For example, the following class might be used to implement a web service that performs a simple addition operation:

@WebServlet(urlPatterns={"/math/*"})
public class MathServlet extends DispatcherServlet {
    @RequestMethod("GET")
    @ResourcePath("/sum")
    public double getSum(double a, double b) {
        return a + b;
    }
}

The following request would cause the method to be invoked, and the service would return the value 6 in response:

GET /math/sum?a=2&b=4

JSONEncoder

The JSONEncoder class, which is used internally by DispatcherServlet to serialize response data, converts return values to their JSON equivalents as follows:

  • CharSequence: string
  • Number: number
  • Boolean: true/false
  • Iterable: array
  • java.util.Map: object

Note that collection types are not required to support random access; iterability is sufficient. This is an important feature, as it allows service implementations to stream result data rather than buffering it in memory before it is written.

ResultSetAdapter

HTTP-RPC’s ResultSetAdapter class implements the Iterable interface and makes each row in a JDBC result set appear as an instance of Map, allowing query results to be efficiently serialized as an array of JSON objects.

For example, consider a web service that returns the result of a SQL query on this table, taken from the MySQL sample database:

CREATE TABLE pet (
    name VARCHAR(20),
    owner VARCHAR(20),
    species VARCHAR(20), 
    sex CHAR(1), 
    birth DATE, 
    death DATE
);

A method to retrieve a list of all pets belonging to a given owner might be implemented as shown below. Note that the example uses HTTP-RPC’s Parameters class to simplify query execution using named parameters rather than positional values. Also note that the method uses JSONEncoder to explicitly write the results to the output stream rather than simply returning the adapter instance, to ensure that the underlying result set is closed and system resources are not leaked:

@RequestMethod("GET")
public void getPets(String owner) throws SQLException, IOException {
    try (Connection connection = DriverManager.getConnection(DB_URL)) {
        Parameters parameters = Parameters.parse("SELECT name, species, sex, birth FROM pet WHERE owner = :owner");

        parameters.put("owner", owner);

        try (PreparedStatement statement = connection.prepareStatement(parameters.getSQL())) {
            parameters.apply(statement);

            try (ResultSet resultSet = statement.executeQuery()) {
                JSONEncoder jsonEncoder = new JSONEncoder();

                jsonEncoder.writeValue(new ResultSetAdapter(resultSet), getResponse().getOutputStream());
            }
        }
    } finally {
        getResponse().flushBuffer();
    }
}

A response produced by the method might look something like this, where each object in the array represents a row from the result set:

[
  {
    "name": "Claws",
    "species": "cat",
    "sex": "m",
    "birth": 763880400000
  },
  {
    "name": "Chirpy",
    "species": "bird",
    "sex": "f",
    "birth": 905486400000
  },
  {
    "name": "Whistler",
    "species": "bird",
    "sex": null,
    "birth": 881643600000
  }
]

With just a few lines of code, query results can be quickly and efficiently returned to the caller, with no intermediate buffering required.

More Information

This article introduced the HTTP-RPC framework and provided an example of how the ResultSetAdapter class can be used to efficiently transform JDBC query results into JSON. For more information, see the project README.