==== tRPC

The tRPC module provides end-to-end type safety by integrating the https://trpc.io/[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:

[dependency, artifactId="jooby-jackson3:Jackson module, jooby-trpc-jackson3:Jackson tRPC JSON implementation, jooby-trpc: tRPC Module"]
.

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
[source,java,role="primary"]
----
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>
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
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>
}
----

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
[source,java,role="primary"]
----
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
  }
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
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
  }
}
----

===== 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
[source,xml,role="primary",subs="verbatim,attributes"]
----
<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>
----

.build.gradle
[source,groovy,role="secondary",subs="verbatim,attributes"]
----
plugins {
  id 'io.jooby.trpc' version "${joobyVersion}"
}

trpc {
  // Optional settings
  jsonLibrary = 'jackson2'
}
----

===== 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:

[source,bash]
----
npm install @trpc/client
----

[source,typescript]
----
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
[source,java,role="primary"]
----
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);
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
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)
})
----

====== 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
[source,xml,role="primary"]
----
<configuration>
  <customTypeMappings>
    <java.util.UUID>string</java.util.UUID>
    <java.math.BigDecimal>number</java.math.BigDecimal>
  </customTypeMappings>
</configuration>
----

.Gradle
[source,groovy,role="secondary"]
----
trpc {
  customTypeMappings = [
    'java.util.UUID': 'string',
    'java.math.BigDecimal': 'number'
  ]
}
----
