Skip to content

Getting Started

The happy path: annotate your C# types, generate TypeScript, use the typed client.

1. Install

bash
# Add the attributes to your API project
dotnet add package Rivet.Attributes --version "*"

# Install the CLI tool
dotnet tool install --global dotnet-rivet

Dependencies:

  • .NET 8+ SDK
  • Rivet.Attributes — marker attributes and contract builders, zero dependencies
  • zod in your consumer project (only required for --compile)

2. Mark your types and endpoints

csharp
// Domain types — discovered transitively from endpoint signatures
public enum Priority { Low, Medium, High, Critical }

public sealed record Email(string Value);    // → branded: string & { __brand: "Email" }

public sealed record Label(string Name, string Color);

// DTOs — discovered transitively from endpoint signatures
public sealed record CreateTaskCommand(
    string Title,
    string? Description,
    Priority Priority,
    Guid? AssigneeId,
    List<string> LabelNames);

public sealed record CreateTaskResult(Guid Id, DateTime CreatedAt);
csharp
[RivetClient]  // auto-discovers all public HTTP methods on this controller
[Route("api/tasks")]
public sealed class TasksController(CreateTaskUseCase createTask) : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(typeof(PagedResult<TaskListItemDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> List(
        [FromQuery] int page,
        [FromQuery] int pageSize,
        [FromQuery] string? status,
        CancellationToken ct)
    {
        // ...
    }

    [HttpPost]
    [ProducesResponseType(typeof(CreateTaskResult), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Create(
        [FromBody] CreateTaskCommand command,
        CancellationToken ct)
    {
        var result = await createTask.ExecuteAsync(command, ct);
        return StatusCode(StatusCodes.Status201Created, result);
    }
}

All types are discovered transitively — request types ([FromBody]), response types ([ProducesResponseType] or typed returns), and everything they reference (enums, VOs, nested records). [RivetType] is only needed for types that aren't reachable from any endpoint or contract (e.g., a shared DTO used only in frontend logic).

3. Generate

bash
dotnet rivet --project path/to/Api.csproj --output ./generated

4. What it produces

typescript
// Generated by Rivet — do not edit

export type Priority = "Low" | "Medium" | "High" | "Critical";
typescript
// Generated by Rivet — do not edit

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

export type CreateTaskCommand = {
  title: string;
  description: string | null;
  priority: Priority;
  assigneeId: string | null;
  labelNames: string[];
};

export type CreateTaskResult = {
  id: string;
  createdAt: string;
};
typescript
// Generated by Rivet — do not edit

export function list(
  page: number, pageSize: number, status: string | null
): Promise<PagedResult<TaskListItemDto>>;

export type CreateResult =
  | { status: 201; data: CreateTaskResult; response: Response }
  | { status: 422; data: void; response: Response };

export function create(command: CreateTaskCommand): Promise<CreateTaskResult>;
export function create(
  command: CreateTaskCommand, opts: { unwrap: false }
): Promise<CreateResult>;

Full output structure:

generated/
├── types/
│   ├── index.ts              # barrel: export * as common, export * as domain, ...
│   ├── common.ts             # types referenced across multiple groups
│   ├── controllers.ts        # types grouped by C# namespace
│   └── createTask.ts
├── rivet.ts                  # configureRivet(), rivetFetch, RivetError, RivetResult
├── client/
│   ├── index.ts              # barrel: export * as tasks, export * as members
│   └── tasks.ts              # overloaded functions with typed error responses
├── schemas.ts                # (after --compile) JSON Schema definitions
└── validators.ts             # (after --compile) Zod assertion wrappers

Types are split by C# namespace. Types referenced across multiple groups are promoted to common.ts. Barrel exports let consumers import from types/index.js — the grouping is purely for navigating the generated code.

5. Use

typescript
import { configureRivet } from "~/generated/rivet";
import { tasks } from "~/generated/client";

// Configure once at app startup
configureRivet({
  baseUrl: "http://localhost:5000",
  headers: () => ({ Authorization: `Bearer ${token}` }),
});

// Returns T directly — throws RivetError on non-2xx
const result = await tasks.create({
  title: "Fix the thing",
  priority: "High",
  assigneeId: null,
  description: null,
  labelNames: ["bug"],
});
console.log(result.id);        // string
console.log(result.createdAt); // string

Next steps

  • Follow the tutorial — build a minimal API from scratch with contracts, branded VOs, file uploads, and a typed client
  • Add runtime validation with --compile for type assertions at the network boundary
  • Define contracts for a decoupled API surface with compile-time enforcement
  • Generate an OpenAPI spec alongside your TypeScript output
  • Import an OpenAPI spec to generate C# contracts from an external API