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:
<!-- 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.
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)
}
-
Install a supported JSON engine (
Jackson3Module,Jackson2Modulefor Jackson 2, orAvajeJsonbModule). -
Install the corresponding bridge module (
TrcJackson3Module,TrpcJackson2Module, orTrpcAvajeJsonbModule). -
Install the core tRPC extension (
TrpcModule). -
Register your
@Trpcannotated 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 toGET) and@Trpc.Mutation(maps toPOST). -
Hybrid Annotations: Combine the base
@Trpcannotation with Jooby’s standard HTTP annotations. A@GETresolves to a tRPC query, while state-changing methods (@POST,@PUT,@DELETE) resolve to tRPC mutations.
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.
<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:
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:
<configuration>
<customTypeMappings>
<java.util.UUID>string</java.util.UUID>
<java.math.BigDecimal>number</java.math.BigDecimal>
</customTypeMappings>
</configuration>