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.
Dependencies:
.
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)
}import io.jooby.kt.Kooby
import io.jooby.json.Jackson2Module
import io.jooby.trpc.TrpcModule
import io.jooby.trpc.TrpcJackson2Module
{
install(Jackson3Module()) // (1)
install(TrpcJackson3Module()) // (2)
install(TrpcModule(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).
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
}
}import io.jooby.annotation.trpc.Trpc
import io.jooby.annotation.DELETE
data class Movie(val id: Int, val title: String, val year: Int)
@Trpc("movies") // Defines the 'movies' namespace
class MovieService {
// 1. Explicit tRPC Query
@Trpc.Query
fun getById(id: Int): Movie {
return Movie(id, "Pulp Fiction", 1994)
}
// 2. Explicit tRPC Mutation
@Trpc.Mutation
fun create(movie: Movie): Movie {
// Save to database logic here
return movie
}
// 3. Hybrid Mutation
@Trpc
@DELETE
fun delete(id: Int) {
// Delete from database
}
}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>plugins {
id 'io.jooby.trpc' version "${joobyVersion}"
}
trpc {
// Optional settings
jsonLibrary = 'jackson2'
}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/clientimport { 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})`);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);
}import io.jooby.kt.Kooby
import io.jooby.trpc.TrpcModule
import io.jooby.trpc.TrpcErrorCode
class App : Kooby({
install(TrpcModule())
// Map your custom business exception to a standard tRPC error code
services.mapOf(Class::class.java, TrpcErrorCode::class.java)
.put(IllegalArgumentException::class.java, TrpcErrorCode.BAD_REQUEST)
.put(MovieNotFoundException::class.java, TrpcErrorCode.NOT_FOUND)
})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>trpc {
customTypeMappings = [
'java.util.UUID': 'string',
'java.math.BigDecimal': 'number'
]
}