3 min read

How to generate typescript definitions and http client for spring boot

The most time-consuming aspect of writing full-stack applications with separate backend and frontend is having to define all types twice, particularly when working with type-safe languages.

Fortunately, there are ways to streamline this process. Today, I'll share how you can achieve this for Spring Boot + NextJS applications (though this approach works with other frameworks as well).

We'll accomplish this using a Maven plugin (a Gradle version is also available) and a few scripts.

<plugin>
        <groupId>cz.habarta.typescript-generator</groupId>
        <artifactId>typescript-generator-maven-plugin</artifactId>
        <version>${typescript-generator.version}</version>
        <executions>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
                <phase>process-classes</phase>
            </execution>
        </executions>
        <configuration>
            <jsonLibrary>jackson2</jsonLibrary>
            <generateSpringApplicationClient>true</generateSpringApplicationClient>
            <classesWithAnnotations>
                <classesWithAnnotation>com.example.backend.util.Client</classesWithAnnotation>
            </classesWithAnnotations>
            <outputKind>module</outputKind>
            <outputFileType>implementationFile</outputFileType>
            <mapPackagesToNamespaces>false</mapPackagesToNamespaces>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>cz.habarta.typescript-generator</groupId>
                <artifactId>typescript-generator-spring</artifactId>
                <version>${typescript-generator.version}</version>
            </dependency>
        </dependencies>
    </plugin>

In this configuration, I'm using the classesWithAnnotations setting, which means I've created my own marker interface to specify which types I want to export. While this is my preferred approach, there are other options available - you can use patterns or exact class names. For a complete list of configuration options, visit: https://www.habarta.cz/typescript-generator/maven/typescript-generator-maven-plugin/generate-mojo.html

Here's my marker interface implementation and usage:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Client {

}

// Now to use it, you would put it on any type you want(including rest controllers if you want http client methods generated)

@Client
public class CreateUserRequest { 
...

@RestController
@Client
public class UsersController {
...

That covers the backend setup. You can verify it works by running:

./mvnw typescript-generator:generate

With the current configuration, the generated types will be located in:

/target/typescript-generator/backend.ts

Getting the types to our client code

To integrate these types into your frontend code, we can create a simple script that generates the files on the backend and imports them into the frontend project. Here's how to set it up in your package.json file:

"copy-types": "copyfiles -f ../backend/target/typescript-generator/backend.ts ./models/",
"generate-types": "cd ../backend && mvnw.cmd typescript-generator:generate",
"update-types": "npm run generate-types && npm run copy-types",

Before you run it, install the copyfiles:

npm install --save-dev copyfiles

Then run:

npm run update-types

This will generate the types and import them into your project, making them ready to use.

One of the best features is that we also get an HTTP client with all our exposed controller methods. Here's how to create an instance of the client:

import { RestApplicationClient } from '@/models/backend'
import Axios from 'axios'
require('dotenv').config()

const httpClient = Axios.create({
  baseURL: process.env.NEXT_PUBLIC_BASE_URL!, 
  headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/json',
      'Accept': 'application/json'
  },
  withCredentials: true,
  xsrfCookieName: 'XSRF-TOKEN',
  withXSRFToken: true,
})

export const restClient = new RestApplicationClient(httpClient)

Here's an example of how to use the client:

  const logout = async () => {
    if (!error) {
      await restClient.logout().then(() => mutate()); <--- logout() is an endpoint on our server, we don't need to know the path
    }

    window.location.pathname = "/auth/login";
  };

The best part? You get complete type safety throughout your application.

And that's it. I hope you will find this useful, and if you do, then consider giving this repository a star.

https://github.com/vojtechhabarta/typescript-generator