Rainer Hahnekamp

Typesafe Endpoints with TypeScript and Java

Introduction

In Single-Page-Applications (SPAs), the server usually provides endpoints (API) and does no rendering at all.

With TypeScript you can ensure the endpoints are called with the correct types. In this posting I will show how to convert message types of an API to TypeScript types automatically within the context of Spring MVC.

TypeScript lets you develop JavaScript in a typesafe mode. By that I mean it prevents you from introducing bugs due to the use of the wrong types.

Similar to header files in C, type definition files exist for libraries written in JavaScript, like jQuery, that enhance the libraries with type information

Location of typesafe endpoint

Our endpoints will have such type definitions as well. Instead of fetching them via npm , however, we will generate them dynamically.

The backend application

Given a Spring MVC application with an endpoint that returns a list of available products:

@RestController
public class MainController {
  @RequestMapping(path = "products")
  public ProductsResponse getProducts() {
    Product foo = new Product();
    foo.setAmountInStock(5);
    /* setting other properties */

    Product bar = new Product();
    bar.setAmountInStock(3);
    /* setting other properties */

    ProductsResponse returner = new ProductsResponse();
    returner.setProducts(Arrays.asList(foo, bar));

    return returner;
  }
}

@Data
public class BaseRequest {
  private Region region;
}

public enum Region {
  CENTRAL_EUROPE,
  SCANDINAVIA,
  UK
}

@Data
public class ProductsRequest extends BaseRequest {
  private Instant validFrom;
  private Date validTo;
}

We don’t query a database in our example. We just return some test data. As you can see, the ProductsRequest has two different types for representing a date: java.time.Instant and java.util.Date. Furthermore, it inherits from a BaseRequest since, in our hypothetical scenario, all requests must indicate the context in the form of its market region.

The response contains an array of the available products for that region:

@Data
public class ProductsResponse {
  private List<Product> products;
}

@Data
public class Product {
  private String code;
  private String groupName;
  private int amountInStock;
  private BigDecimal price;
}

So next to the conversion of the default data types (String, int, boolean) the generator has to provide a solution for following cases:

Generating the Types

The generator itself is available as a gradle or maven plugin on https://github.com/vojtechhabarta/typescript-generator. Since we are using maven in our example, we add the following plugin-section to the pom.xml:

<plugin>
  <groupId>cz.habarta.typescript-generator</groupId>
  <artifactId>typescript-generator-maven-plugin</artifactId>
  <version>1.25.322</version>
  <executions>
    <execution>
      <id>generate</id>
      <goals>
        <goal>generate</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <jsonLibrary>jackson2</jsonLibrary>
    <classes>
      <class>com.hahnekamp.message.ProductsRequest</class>
      <class>com.hahnekamp.message.ProductsResponse</class>
    </classes>
    <customTypeMappings>
      <mapping>java.time.Instant:Date</mapping>
    </customTypeMappings>
    <outputFile>app/src/endpoints.d.ts</outputFile>
    <outputKind>global</outputKind>
    <namespace>endpoint</namespace>
  </configuration>
</plugin>

What’s going on here? We define our request and response type as classes to be mapped. Although the sample code explicitly maps each class, you could use glob patterns instead. You can create customized mappings for incompatible types. We do this in our example by converting java.time.Instant to a Date type in the customTypeMappings tag. Eventually we have to set the name of the TypeScript declaration file we will use. In our case we write it into endpoint.d.ts.

The plugin executes directly after the maven build phase “compile”. In our case, this means that each time we execute spring-boot:run we get an updated typings file. We can manually trigger the code generation by running:

./mvnw typescript-generator:generate

The generated file will look like:

declare namespace endpoint {

  interface ProductsRequest extends BaseRequest {
    validFrom: Date;
    validTo: Date;
  }

  interface ProductsResponse {
    products: Product[];
  }

  interface BaseRequest {
    region: Region;
  }

  interface Product {
    code: string;
    groupName: string;
    amountInStock: number;
    price: number;
  }

  type Region = "CENTRAL_EUROPE" | "SCANDINAVIA" | "UK";

}

As you can see, the generator mapped the required classes BaseRequest and Enumeration automatically even though we did not set in the pom.xml.

Integrating the types into our code

We can now test it by creating a minimalistic node script in TypeScript. We create an index.ts in the folder app/src where the endpoint.d.ts is already stored with the following content:

import * as request from "request";
import * as _ from "lodash";
import ProductsRequest = endpoint.ProductsRequest;
import ProductsResponse = endpoint.ProductsResponse;
import {error} from "util";


function requestServer(data: ProductsRequest): Promise<ProductsResponse> {
  return new Promise<ProductsResponse>((resolve, reject) =>
    request.get("http://localhost:8080/products", (error, response) => {
      if (error) {
        reject(error);
      }
      else {
        resolve(JSON.parse(response.body));
      }
    })
  );
}

let requestData: ProductsRequest = {
  region: "CENTRAL_EUROPE",
  validFrom: new Date(2017, 0, 1),
  validTo: new Date(2017, 1, 1)
};

requestServer(requestData)
  .then(
    productsResponse => _.map(productsResponse.products)
      .forEach(product => console.log(product.code + " á " + product.price)),
    error => console.error("Error occured. Please check that the server is running")
  );

Please note that the whole source code is available on https://github.com/rainerhahnekamp/java-typescript-code-generator.

Final Thoughts

Your SPAs probably have domain objects you are using in TypeScript that are very similar to endpoint types. I think the best thing to do in such a situation is to stick with what you already have. Use the generated ones only for the services that are actually doing the endpoint communication.

For example, you could already have an interface for the type product that consists of code and price. You don’t even need to to use a mapping function client-side:

interface DomainProduct {
 code: string,
 price: number
}
let endpointProduct: Product =
 {code: "Nertsedus D-Class", groupName: "cars", amountInStock: 10, price: 10.53};
let product: DomainProduct = endpointProduct;

After all, the reason for generating the typings is to make sure we are calling the endpoint correctly. The structure of our endpoint should not dictate the structure of the domain models on the client side. We still strive for loose coupling.

In my example I’ve used Spring MVC that required me to indicate which classes I want to have converted. If your endpoint is running on an JAX-RS implementation like Jersey, then the generator can detect and convert the message types on its own.

You have to be aware that your IDE can’t provide full refactoring. For example, if you rename a class it would be reflected in endpoint.d.ts but all other typescript files referring to that class will fail. You have to fix them manually.

For my closing advice: Look out when using “TypeScripted” libraries like Angular. Its http service already supports requests with Generic types.