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-rivetDependencies:
- .NET 8+ SDK
Rivet.Attributes— marker attributes and contract builders, zero dependencieszodin 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 ./generated4. 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 wrappersTypes 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); // stringNext steps
- Follow the tutorial — build a minimal API from scratch with contracts, branded VOs, file uploads, and a typed client
- Add runtime validation with
--compilefor 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
