Jooby

tRPC

The tRPC module provides end-to-end type safety by integrating the tRPC protocol directly into Jooby.

Because tRPC is provided in its own module, you will need to add the jooby-trpc-* dependencies to your project. This integration allows you to write standard Java/Kotlin controllers and consume them directly in the browser using the official @trpc/client—complete with 100% type safety, autocomplete, and zero manual client generation.

Usage

Dependencies:

Maven
Gradle
<!-- Jackson module-->
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-jackson3</artifactId>
  <version>4.3.0</version>
</dependency>

<!-- Jackson tRPC JSON implementation-->
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-trpc-jackson3</artifactId>
  <version>4.3.0</version>
</dependency>

<!--  tRPC Module-->
<dependency>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-trpc</artifactId>
  <version>4.3.0</version>
</dependency>

Installing tRPC in your Jooby app is a 3-step process. Because tRPC relies heavily on JSON serialization to communicate with the frontend client, you must install a JSON module, followed by its corresponding tRPC bridge module, and finally the core TrpcModule.

Java
Kotlin
import io.jooby.Jooby;
import io.jooby.json.Jackson2Module;
import io.jooby.trpc.TrpcModule;
import io.jooby.trpc.TrpcJackson2Module;

{
  install(new Jackson3Module());                    // (1)

  install(new TrpcJackson3Module());                // (2)

  install(new TrpcModule(new MovieServiceTrpc_())); // (3)
}
  1. Install a supported JSON engine (Jackson3Module, Jackson2Module for Jackson 2, or AvajeJsonbModule).

  2. Install the corresponding bridge module (TrcJackson3Module, TrpcJackson2Module, or TrpcAvajeJsonbModule).

  3. Install the core tRPC extension (TrpcModule).

  4. Register your @Trpc annotated controllers (using the APT generated route).

Writing a Service

You can define your procedures using explicit tRPC annotations or a hybrid approach combining tRPC with standard HTTP methods:

  • Explicit Annotations: Use @Trpc.Query (maps to GET) and @Trpc.Mutation (maps to POST).

  • Hybrid Annotations: Combine the base @Trpc annotation with Jooby’s standard HTTP annotations. A @GET resolves to a tRPC query, while state-changing methods (@POST, @PUT, @DELETE) resolve to tRPC mutations.

Java
Kotlin
import io.jooby.annotation.trpc.Trpc;
import io.jooby.annotation.DELETE;

public record Movie(int id, String title, int year) {}

@Trpc("movies") // Defines the 'movies' namespace
public class MovieService {

  // 1. Explicit tRPC Query
  @Trpc.Query
  public Movie getById(int id) {
    return new Movie(id, "Pulp Fiction", 1994);
  }

  // 2. Explicit tRPC Mutation
  @Trpc.Mutation
  public Movie create(Movie movie) {
    // Save to database logic here
    return movie;
  }

  // 3. Hybrid Mutation
  @Trpc
  @DELETE
  public void delete(int id) {
    // Delete from database
  }
}
Build Configuration

To generate the trpc.d.ts TypeScript definitions, you must configure the Jooby build plugin for your project. The generator parses your source code and emits the definitions during the compilation phase.

pom.xml
build.gradle
<plugin>
  <groupId>io.jooby</groupId>
  <artifactId>jooby-maven-plugin</artifactId>
  <version>${jooby.version}</version>
  <executions>
    <execution>
      <goals>
        <goal>trpc</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <jsonLibrary>jackson2</jsonLibrary>
    <outputDir>${project.build.outputDirectory}</outputDir>
  </configuration>
</plugin>
Consuming the API (Frontend)

Once the project is compiled, the build plugin generates a trpc.d.ts file containing your exact AppRouter shape. You can then use the official client in your TypeScript frontend:

npm install @trpc/client
import { createTRPCProxyClient, httpLink } from '@trpc/client';
import type { AppRouter } from './target/classes/trpc'; // Path to generated file

// Initialize the strongly-typed client
export const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpLink({
      url: 'http://localhost:8080/trpc',
    }),
  ],
});

// 100% Type-safe! IDEs will autocomplete namespaces, inputs, and outputs.
const movie = await trpc.movies.getById.query(1);
console.log(`Fetched: ${movie.title} (${movie.year})`);
Advanced Configuration
Custom Exception Mapping

The tRPC protocol expects specific JSON-RPC error codes (e.g., -32600 for Bad Request). TrpcModule automatically registers a specialized error handler to format these errors.

If you throw custom domain exceptions, you can map them directly to tRPC error codes using the service registry so the frontend client receives the correct error state:

Java
Kotlin
import io.jooby.trpc.TrpcModule;
import io.jooby.trpc.TrpcErrorCode;

{
  install(new TrpcModule());

  // Map your custom business exception to a standard tRPC error code
  getServices().mapOf(Class.class, TrpcErrorCode.class)
    .put(IllegalArgumentException.class, TrpcErrorCode.BAD_REQUEST)
    .put(MovieNotFoundException.class, TrpcErrorCode.NOT_FOUND);
}
Custom TypeScript Mappings

Sometimes you have custom Java types (like java.util.UUID or java.math.BigDecimal) that you want translated into specific TypeScript primitives. You can define these overrides in your build tool:

Maven
Gradle
<configuration>
  <customTypeMappings>
    <java.util.UUID>string</java.util.UUID>
    <java.math.BigDecimal>number</java.math.BigDecimal>
  </customTypeMappings>
</configuration>