dotnet-rivet
0.32.3
dotnet tool install --global dotnet-rivet --version 0.32.3
dotnet new tool-manifest
dotnet tool install --local dotnet-rivet --version 0.32.3
#tool dotnet:?package=dotnet-rivet&version=0.32.3
nuke :add-package dotnet-rivet --version 0.32.3
<p align="center"> <img src="logo.png" alt="Rivet" width="200" /> <h1 align="center">Rivet</h1> <p align="center"> <a href="https://www.nuget.org/packages/Rivet.Attributes"><img src="https://img.shields.io/nuget/v/Rivet.Attributes?label=Rivet.Attributes" alt="NuGet" /></a> <a href="https://www.nuget.org/packages/dotnet-rivet"><img src="https://img.shields.io/nuget/v/dotnet-rivet?label=dotnet-rivet" alt="NuGet" /></a> <a href="https://github.com/maxanstey-meridian/rivet/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License" /></a> </p> </p>
End-to-end type safety between .NET and TypeScript. No drift, no schema files, no codegen config.
If .NET can express a type that can be serialised on the wire, Rivet can make it TypeScript on the other side. It maps exactly what can survive a JSON boundary — no more, no less.
oRPC gives you this when your server is TypeScript. Rivet gives you the same DX when your server is .NET.
New here? Follow the Tutorial: Zero to Typed Client —
dotnet new webapito a fully typed TS client in under 5 minutes.
Install
dotnet add package Rivet.Attributes --version "*"
dotnet tool install --global dotnet-rivet
Mark your C# types → get TypeScript types
[RivetType]
public enum Priority { Low, Medium, High, Critical }
[RivetType]
public sealed record Email(string Value); // single-property → branded
[RivetType]
public sealed record TaskItem(Guid Id, string Title, Priority Priority, Email Author);
[RivetType]
public sealed record ErrorDto(string Code, string Message);
// Generated
export type Priority = "Low" | "Medium" | "High" | "Critical";
export type Email = string & { readonly __brand: "Email" };
export type TaskItem = { id: string; title: string; priority: Priority; author: Email };
export type ErrorDto = { code: string; message: string };
Mark your controllers → get a typed client
[RivetClient]
[Route("api/tasks")]
public sealed class TasksController : ControllerBase
{
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(ErrorDto), 404)]
public async Task<ActionResult<TaskDetailDto>> Get(Guid id, CancellationToken ct) { ... }
}
// Generated — discriminated union, narrowable by status
export type GetResult =
| { status: 200; data: TaskDetailDto; response: Response }
| { status: 404; data: ErrorDto; response: Response }
| { status: Exclude<number, 200 | 404>; data: unknown; response: Response };
const task = await tasks.get(id); // → TaskDetailDto (throws on error)
const result = await tasks.get(id, { unwrap: false }); // → GetResult (no throw)
Typed runtime client
dotnet rivet --project Api.csproj --output ./generated
Rivet generates per-controller client modules with a barrel import, a configureRivet setup function, and a RivetError class — all from your C# source, no config files:
// generated/rivet/client/tasks.ts — one module per controller
import { rivetFetch, type RivetResult } from "../rivet.js";
import type { TaskDetailDto } from "../types/tasks.js";
import type { ErrorDto } from "../types/common.js";
export type GetResult =
| { status: 200; data: TaskDetailDto; response: Response }
| { status: 404; data: ErrorDto; response: Response }
| { status: Exclude<number, 200 | 404>; data: unknown; response: Response };
export function get(id: string): Promise<TaskDetailDto>;
export function get(id: string, opts: { unwrap: false }): Promise<GetResult>;
export async function get(id: string, opts?: { unwrap?: boolean }): Promise<TaskDetailDto | GetResult> {
// ...
}
// generated/rivet/client/index.ts — barrel for all controllers
export * as tasks from "./tasks.js";
export * as members from "./members.js";
Wire it once, then call your API with full type safety:
import { configureRivet } from "./generated/rivet/rivet.js";
import { tasks } from "./generated/rivet/client/index.js";
configureRivet({ baseUrl: "https://api.example.com" });
// Default: throws RivetError on non-2xx
const task = await tasks.get("some-id"); // → TaskDetailDto
// Discriminated union: handle each status explicitly
const result = await tasks.get("some-id", { unwrap: false });
if (result.status === 200) {
console.log(result.data.title); // TaskDetailDto
} else if (result.status === 404) {
console.log(result.data.message); // ErrorDto
}
Route parameters, query strings, file uploads (FormData), and void responses are all handled — the generated client matches the controller signature exactly.
Compile-time enforcement
// Define the API surface — pure Rivet, no ASP.NET dependency
[RivetContract]
public static class MembersContract
{
public static readonly RouteDefinition<List<MemberDto>> List =
Define.Get<List<MemberDto>>("/api/members")
.Summary("List all team members");
}
// Implement it — compiler enforces the return type matches the contract
[HttpGet]
public async Task<IActionResult> List(CancellationToken ct)
=> (await MembersContract.List.Invoke(async () =>
{
return await db.Members.ToListAsync(ct); // must return List<MemberDto>
})).ToActionResult();
// Works with minimal APIs too — .Route avoids duplicating the route string
app.MapGet(MembersContract.List.Route, async (AppDb db, CancellationToken ct) =>
(await MembersContract.List.Invoke(async () =>
{
return await db.Members.ToListAsync(ct);
})).ToResult()); // you write ToResult() once, same pattern as ToActionResult()
Runtime validation with Zod
dotnet rivet --project Api.csproj --output ./generated --compile
Rivet emits Zod 4 validators backed by fromJSONSchema() — a schemas.ts with standalone JSON Schema definitions and a validators.ts that wraps them:
// schemas.ts — standalone JSON Schema, usable with any validator
import type { core } from "zod";
type JSONSchema = core.JSONSchema.JSONSchema;
const $defs: Record<string, JSONSchema> = { /* all type definitions */ };
export const TaskItemSchema: JSONSchema = { "$ref": "#/$defs/TaskItem", "$defs": $defs };
// validators.ts — cached Zod schemas from the JSON Schema definitions
import { fromJSONSchema, z } from "zod";
import { TaskItemSchema } from "./schemas.js";
const _assertTaskItem = fromJSONSchema(TaskItemSchema);
export const assertTaskItem = (data: unknown): TaskItem => _assertTaskItem.parse(data) as TaskItem;
Every API response is validated at the network boundary — not just primitives, but full object shapes, nested types, and unions. If the server sends unexpected data, you get a clear error immediately — not a silent undefined three components later. Requires zod in your consumer project.
You can also emit just the schemas without validation wiring:
dotnet rivet --project Api.csproj --output ./generated --jsonschema
This writes schemas.ts only — use it with fromJSONSchema(), ajv, or any JSON Schema consumer.
OpenAPI import
Another team owns the API? Import their OpenAPI spec, get typed C# contracts, feed them back into the pipeline. The compiler tells you what broke when the upstream spec changes.
{
"components": {
"schemas": {
"TaskDto": {
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"title": { "type": "string" },
"priority": { "$ref": "#/components/schemas/Priority" }
},
"required": ["id", "title", "priority"]
},
"Priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"]
}
}
},
"paths": {
"/api/tasks": {
"get": {
"tags": ["Tasks"],
"summary": "List all tasks",
"responses": {
"200": {
"content": { "application/json": { "schema": {
"type": "array", "items": { "$ref": "#/components/schemas/TaskDto" }
} } }
}
}
}
}
}
}
dotnet rivet --from-openapi spec.json --namespace MyApp.Contracts --output ./src/
// Generated — sealed records, enums, typed contract with builder chain
public enum Priority { Low, Medium, High, Critical }
public sealed record TaskDto(Guid Id, string Title, Priority Priority);
[RivetContract]
public static class TasksContract
{
public static readonly RouteDefinition<List<TaskDto>> List =
Define.Get<List<TaskDto>>("/api/tasks")
.Summary("List all tasks");
}
The importer handles JSON, form-encoded, binary (application/octet-stream → IFormFile), and text (text/* → string) content types. Endpoints with unsupported or schema-less content types are still generated but annotated with a // [rivet:unsupported ...] comment — see the OpenAPI Import guide for details.
Check contract coverage
dotnet rivet --project Api.csproj --check
warning: [MissingImplementation] MembersContract.Remove: expected DELETE /api/members/{id}, got (none)
warning: [RouteMismatch] MembersContract.UpdateRole: expected /api/members/{id}/role, got /api/members/{id}/update-role
Coverage: 2/4 endpoints covered, 1 mismatch(es), 1 missing.
Verifies that every contract endpoint has a matching handler implementation, with correct HTTP method and route. Useful in CI to catch missing or mismatched handlers.
List your routes
dotnet rivet --project Api.csproj --routes
Method Route Handler
────── ───────────────────────── ───────
GET /api/members members.list
POST /api/members members.invite
DELETE /api/members/{id} members.remove
PUT /api/members/{id}/role members.updateRole
4 route(s).
Documentation
Guides, reference, and architecture at maxanstey-meridian.github.io/rivet.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.32.3 | 27 | 4/8/2026 |
| 0.32.2 | 44 | 4/7/2026 |
| 0.32.1 | 37 | 4/7/2026 |
| 0.32.0 | 35 | 4/7/2026 |
| 0.31.0 | 89 | 4/6/2026 |
| 0.30.0 | 95 | 3/30/2026 |
| 0.29.0 | 90 | 3/30/2026 |
| 0.28.0 | 94 | 3/29/2026 |
| 0.27.0 | 92 | 3/24/2026 |
| 0.26.0 | 77 | 3/23/2026 |
| 0.25.0 | 82 | 3/23/2026 |
| 0.24.0 | 78 | 3/21/2026 |
| 0.22.2 | 82 | 3/20/2026 |
| 0.22.1 | 79 | 3/20/2026 |
| 0.22.0 | 79 | 3/20/2026 |
| 0.21.3 | 78 | 3/20/2026 |
| 0.21.2 | 73 | 3/20/2026 |
| 0.21.1 | 75 | 3/20/2026 |
| 0.20.1 | 73 | 3/20/2026 |
| 0.20.0 | 80 | 3/19/2026 |