Skip to content

RivetEnd-to-end type safety between .NET and TypeScript

No drift, no schema files, no codegen config.

Rivet

Your C# types...

csharp
// Enums → string union types
public enum Priority { Low, Medium, High, Critical }
public enum WorkItemStatus { Draft, Open, InProgress, Review, Done, Cancelled }

// Multi-property records → object types
public sealed record Label(string Name, string Color);

// Single-property records → branded primitives
public sealed record Email(string Value);
public sealed record TaskId(Guid Value);
csharp
// Nested records with nullables, enums, and value objects
public sealed record TaskDetailDto(
    Guid Id,
    string Title,
    string? Description,         // → string | null
    WorkItemStatus Status,       // → "Draft" | "Open" | ...
    Priority Priority,           // → "Low" | "Medium" | ...
    string? AssigneeName,
    List<Label> Labels,          // → Label[]
    List<CommentDto> Comments,
    DateTime CreatedAt,          // → string (ISO 8601)
    DateTime? CompletedAt);      // → string | null

// Generic wrapper — preserved as generic in TS
[RivetType]
public sealed record PagedResult<T>(
    List<T> Items, int TotalCount, int Page, int PageSize);

...become TypeScript types

typescript
// Generated by Rivet — do not edit

export type Priority = "Low" | "Medium" | "High" | "Critical";
export type WorkItemStatus = "Draft" | "Open" | "InProgress"
  | "Review" | "Done" | "Cancelled";

export type Label = {
  name: string;
  color: string;
};

export type Email = string & { readonly __brand: "Email" };
export type TaskId = string & { readonly __brand: "TaskId" };
typescript
// Generated by Rivet — do not edit

import type { Label, Priority, WorkItemStatus } from "./common.js";

export type TaskDetailDto = {
  id: string;
  title: string;
  description: string | null;
  status: WorkItemStatus;
  priority: Priority;
  assigneeName: string | null;
  labels: Label[];
  comments: CommentDto[];
  createdAt: string;
  completedAt: string | null;
};
typescript
// Generated by Rivet — do not edit

export type PagedResult<T> = {
  items: T[];
  totalCount: number;
  page: number;
  pageSize: number;
};

Your controllers...

csharp
[RivetClient]                            // ← auto-discover all endpoints
[Route("api/tasks")]
public sealed class TasksController(CreateTaskUseCase createTask) : ControllerBase
{
    [HttpGet]                            // GET /api/tasks?page=&pageSize=&status=
    [ProducesResponseType(typeof(PagedResult<TaskListItemDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> List(
        [FromQuery] int page, [FromQuery] int pageSize,
        [FromQuery] string? status, CancellationToken ct) { ... }

    [HttpGet("{id:guid}")]               // GET /api/tasks/{id} — two possible responses
    [ProducesResponseType(typeof(TaskDetailDto), StatusCodes.Status200OK)]
    [ProducesResponseType(typeof(NotFoundDto), StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Get(Guid id, CancellationToken ct) { ... }

    [HttpPost]                           // POST /api/tasks — typed body + 201 response
    [ProducesResponseType(typeof(CreateTaskResult), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Create(
        [FromBody] CreateTaskCommand command, CancellationToken ct) { ... }

    [HttpDelete("{id:guid}")]            // DELETE → renamed to remove() in TS
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(Guid id, CancellationToken ct) { ... }
}

...become a typed client

typescript
// Generated by Rivet — do not edit

// Typed query params, fully unwrapped return type
export function list(
  page: number, pageSize: number, status: string | null
): Promise<PagedResult<TaskListItemDto>>;

// Discriminated union for multi-response endpoints
export type GetResult =
  | { status: 200; data: TaskDetailDto; response: Response }
  | { status: 404; data: NotFoundDto; response: Response };

export function get(id: string): Promise<TaskDetailDto>;
export function get(id: string, opts: { unwrap: false }): Promise<GetResult>;

// Typed request body, 201 default
export function create(command: CreateTaskCommand): Promise<CreateTaskResult>;

// delete → remove (reserved word in TS)
export function remove(id: string): Promise<void>;

Why Rivet?

oRPC gives you end-to-end type safety when your server is TypeScript. Rivet gives you the same DX when your server is .NET.

Unlike OpenAPI-based generators (NSwag, Kiota, Kubb), Rivet reads Roslyn's full type graph — nullable annotations, sealed records, string enum unions, generic type parameters — and produces richer TypeScript types than any JSON schema intermediary can represent.

Rivet is not just a client generator. Every type reachable from an endpoint or contract — records, enums, value objects, generics — is emitted automatically. No per-type annotations needed. For types that aren't reachable from any endpoint (shared frontend-only DTOs), [RivetType] opts them in explicitly.