Skip to content

feature: end-to-end type safety with native tRPC (trpc.io) integration#3867

Merged
jknack merged 20 commits intomainfrom
3863
Mar 8, 2026
Merged

feature: end-to-end type safety with native tRPC (trpc.io) integration#3867
jknack merged 20 commits intomainfrom
3863

Conversation

@jknack
Copy link
Member

@jknack jknack commented Mar 7, 2026

Overview

This PR introduces native, out-of-the-box support for the tRPC (trpc.io) protocol in Jooby.

Historically restricted to the Node.js/TypeScript ecosystem, this feature allows developers 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.

Key Features & Implementation

  • Annotation Processor (APT) Mapping:
    • Added @Trpc, @Trpc.Query, and @Trpc.Mutation annotations.
    • Automatically maps tRPC queries to HTTP GET and mutations to HTTP POST at the routing layer.
    • Seamlessly integrates with standard REST annotations (@POST, @PUT, @DELETE resolve as tRPC mutations).
    • Automatically unpacks multi-argument Java methods from tRPC JSON arrays (tuples) while safely ignoring framework-injected parameters like io.jooby.Context and Kotlin Continuation.
  • TypeScript Generator (TrpcGenerator):
    • Custom engine wrapping typescript-generator to emit a strictly typed AppRouter and associated data models (DTOs).
    • Implemented a "reactive firewall" that deeply unwraps async types (CompletableFuture, Mono, Single, Uni, etc.) so internal JVM wrapper types never leak into the TypeScript definitions.
    • Generates a fully deterministic, alphabetically sorted trpc.d.ts file grouped by namespaces.
  • Build Tooling:
    • Maven: Added TrpcMojo tied to the process-classes phase.
    • Gradle: Added TrpcTask to the jooby-gradle-plugin.
    • Supports configuration including custom type mappings, Jackson/JSON-B/Gson specific parsing, and Date/Enum formatting options.

Example

1. The Java Controller
Write standard Jooby controllers using the new @Trpc annotations.

import io.jooby.annotation.Trpc;

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

@Trpc("movies")
public class MovieService {

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

  @Trpc.Mutation
  public Movie create(Movie movie) {
    // save to database
    return movie;
  }
}

The Generated TypeScript (trpc.d.ts)

The build plugin automatically extracts the DTOs and generates the exact AppRouter shape expected by the frontend @trpc/client.

/* tslint:disable */
/* eslint-disable */

export interface Movie {
    id: number;
    title: string;
    year: number;
}

// --- tRPC Router Mapping ---

export type AppRouter = {
  movies: {
    // queries
    getById: { input: number; output: Movie };

    // mutations
    create: { input: Movie; output: Movie };
  };
};

jknack added 18 commits March 2, 2026 13:15
- generate `d.ts` file from MVC routes
- introduce `@Trpc`, `@Trpc.Query` and `@Trpc.Mutation`
- implement `GET` on APT
- ref #3863
  - it uses a cool parser which allow to provide custom implemantation of it (Jackson/Avajeb)
  - remove hardcode dependency to Jackson
  - implement Java/Kotlin code generation
  - implement success, void/empty and error reponses
  - add protocol integration test
  - ref #3863
This major refactor unifies the tRPC route generation with the standard
MVC routing flow, eliminating code duplication and properly integrating
tRPC endpoints with Jooby's native reactive pipeline.

Key changes:
* Unify Code Generation: Merged `generateTrpcMethod` into `generateHandlerCall`
  in `MvcRoute.java` to use a single, robust parameter extraction and
  method invocation flow.
* Reactive Pipeline Support: Fixed an architectural issue where reactive types
  (CompletableFuture, Mono, Uni, etc.) were incorrectly wrapped in a synchronous
  TrpcResponse. The APT now generates `Publisher<TrpcResponse<T>>`, injecting
  the proper `.map(TrpcResponse::of)` operator natively based on the library.
* Hybrid Route Support: `MvcRouter.java` now correctly splits dual-purpose
  methods. If a method is annotated with both a standard HTTP method (e.g., `@GET`)
  and `@Trpc`, the processor generates two separate mappings and handlers (one
  traditional MVC, one strict tRPC).
* tRPC Precedence & Validation: Enforced strict rules for tRPC annotations.
  `@Trpc.Query`/`@Trpc.Mutation` take precedence. A bare `@Trpc` annotation
  now requires an accompanying `@GET` or `@POST` annotation, otherwise it fails
  the build with a descriptive error.
* Kotlin Codegen Fixes: Fixed Return type mismatches for generic parameterized
  types (adding `as Type` casts), fixed missing non-null assertions (`!!`),
  and ensured `Void`/`Unit` methods correctly emit `TrpcResponse.empty()`.
* Cleanup: Removed dead `trpcPath` resolution methods from `MvcContext`
  and `HttpPath`, as well as the obsolete `TrpcMethod` record.
This update removes the mandatory JSON array tuple wrapper for
single-parameter tRPC procedures. Single arguments now map 1:1,
creating a much more natural API for TypeScript clients while
preserving the required tuple array for multi-argument methods.

Key changes:
* TrpcGenerator: Updated TypeScript generation to output raw types
  for single arguments (e.g., `input: Movie`) instead of wrapping
  them in tuples. Removed the `buildClassesDir` property to rely
  strictly on the `ClassLoader`, and aligned annotation precedence
  to match the APT rules.
* APT / MvcRoute: The route generator now evaluates `isTuple` at
  compile-time (`parameters.size() > 1`) and passes this boolean
  directly into the `TrpcParser`, eliminating runtime ambiguity and
  hacky token detection.
* JSON Readers: Updated `JacksonTrpcReader` and `AvajeTrpcReader`
  to accept the `isTuple` flag. The readers now cleanly branch between
  iterating over an array or reading a raw value directly from the root.
* Testing: Overhauled `TrpcProtocolTest` to validate the new
  seamless protocol, covering raw objects, single-argument collections,
  multi-argument tuples, reactive payloads, and URL encoding compliance.
This update hardens the TypeScript generation pipeline to prevent
internal JVM and reactive wrapper types from leaking into the client
definitions, and expands tRPC mutation support to cover all standard
state-changing HTTP verbs.

Key changes:
* TrpcGenerator: Fixed a bug where `CompletableFuture`, `Mono`, `Single`,
  and other async wrappers were being emitted in the `trpc.d.ts` output.
  Implemented a deep recursive unwrapping mechanism to extract the true
  underlying DTOs from complex generic, wildcard, and array signatures.
* TrpcGenerator: Added a Jackson "firewall" to the typescript-generator
  settings, explicitly mapping all known async wrappers to `any` to
  prevent indirect discovery via reflection.
* TrpcGenerator: Dynamically extracts generic type parameters (e.g.,
  `Future<V>`) via reflection to satisfy typescript-generator's strict
  custom mapping validation, while safely ignoring wrappers not present
  on the compilation classpath.
* TrpcGenerator: Made the `AppRouter` output 100% deterministic to
  prevent test flakiness. Procedures are now grouped by namespace,
  visually separated into `// queries` and `// mutations`, and sorted
  alphabetically.
* MvcRoute (APT) & TrpcGenerator: Expanded mutation detection. Methods
  annotated with base `@Trpc` alongside `@PUT`, `@PATCH`, or `@DELETE`
  are now correctly categorized as mutations in the TypeScript router
  and mapped to HTTP POST proxy routes by the Jooby annotation processor.
This commit introduces the build tool plugins required to execute the
tRPC TypeScript generator automatically during the build lifecycle, and
fixes a bug regarding how framework-specific parameters are handled in
the tRPC network payload.

Key changes:
* MvcRoute (APT) & TrpcGenerator: Explicitly filter out `io.jooby.Context`
  and `kotlin.coroutines.Continuation` when evaluating tRPC parameters.
  This prevents the backend from incorrectly expecting a JSON array (tuple)
  when a method mixes a single payload argument with framework injections,
  and ensures the TypeScript signatures remain clean.
* Maven Plugin: Added `TrpcMojo` bound to the `process-classes` phase.
  Exposes full configuration for the generator, including `jsonLibrary`
  (Jackson2, JSON-B, Gson), `customTypeMappings`, and `outputDir`
  (defaulting to `target/classes`).
* Gradle Plugin: Added `TrpcTask` and registered the `io.jooby.trpc`
  plugin in the `jooby-gradle-plugin` build script, ensuring feature
  parity with the Maven implementation.
@jknack jknack added this to the 4.0.17 milestone Mar 7, 2026
@jknack jknack merged commit df73717 into main Mar 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant