dotnet-rivet 0.32.3

dotnet tool install --global dotnet-rivet --version 0.32.3
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local dotnet-rivet --version 0.32.3
                    
This package contains a .NET tool you can call from the shell/command line.
#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 Clientdotnet new webapi to 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-streamIFormFile), 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed