{"id":48434257,"url":"https://github.com/maxanstey-meridian/rivet","last_synced_at":"2026-04-06T12:03:31.545Z","repository":{"id":345296104,"uuid":"1185283540","full_name":"maxanstey-meridian/rivet","owner":"maxanstey-meridian","description":"End-to-end type safety for .NET + TypeScript. Reads your Roslyn compilation, emits shared types and a typed fetch client.","archived":false,"fork":false,"pushed_at":"2026-03-29T16:37:08.000Z","size":7068,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-29T18:54:21.392Z","etag":null,"topics":["api-client","aspnetcore","branded-types","code-generation","codegen","csharp","developer-tools","dotnet","full-stack","roslyn","runtime-validation","sealed-records","type-safety","typescript","zod"],"latest_commit_sha":null,"homepage":"https://maxanstey-meridian.github.io/rivet/guides/tutorial","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maxanstey-meridian.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-18T12:28:37.000Z","updated_at":"2026-03-29T16:37:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/maxanstey-meridian/rivet","commit_stats":null,"previous_names":["maxanstey-meridian/rivet"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/maxanstey-meridian/rivet","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxanstey-meridian%2Frivet","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxanstey-meridian%2Frivet/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxanstey-meridian%2Frivet/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxanstey-meridian%2Frivet/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maxanstey-meridian","download_url":"https://codeload.github.com/maxanstey-meridian/rivet/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maxanstey-meridian%2Frivet/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31471470,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-06T08:36:52.050Z","status":"ssl_error","status_checked_at":"2026-04-06T08:36:51.267Z","response_time":112,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-client","aspnetcore","branded-types","code-generation","codegen","csharp","developer-tools","dotnet","full-stack","roslyn","runtime-validation","sealed-records","type-safety","typescript","zod"],"created_at":"2026-04-06T12:02:58.766Z","updated_at":"2026-04-06T12:03:31.531Z","avatar_url":"https://github.com/maxanstey-meridian.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"logo.png\" alt=\"Rivet\" width=\"200\" /\u003e\n  \u003ch1 align=\"center\"\u003eRivet\u003c/h1\u003e\n  \u003cp align=\"center\"\u003e\n    \u003ca href=\"https://www.nuget.org/packages/Rivet.Attributes\"\u003e\u003cimg src=\"https://img.shields.io/nuget/v/Rivet.Attributes?label=Rivet.Attributes\" alt=\"NuGet\" /\u003e\u003c/a\u003e\n    \u003ca href=\"https://www.nuget.org/packages/dotnet-rivet\"\u003e\u003cimg src=\"https://img.shields.io/nuget/v/dotnet-rivet?label=dotnet-rivet\" alt=\"NuGet\" /\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/maxanstey-meridian/rivet/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-blue\" alt=\"License\" /\u003e\u003c/a\u003e\n  \u003c/p\u003e\n\u003c/p\u003e\n\n**End-to-end type safety between .NET and TypeScript.** No drift, no schema files, no codegen config.\n\n\u003e 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.\n\n[oRPC](https://orpc.unnoq.com) gives you this when your server is TypeScript. Rivet gives you the same DX when your server is .NET.\n\n\u003e **New here?** Follow the [**Tutorial: Zero to Typed Client**](https://maxanstey-meridian.github.io/rivet/guides/tutorial) — `dotnet new webapi` to a fully typed TS client in under 5 minutes.\n\n## Install\n\n```bash\ndotnet add package Rivet.Attributes --version \"*\"\ndotnet tool install --global dotnet-rivet\n```\n\n## Mark your C# types → get TypeScript types\n\n```csharp\n[RivetType]\npublic enum Priority { Low, Medium, High, Critical }\n\n[RivetType]\npublic sealed record Email(string Value); // single-property → branded\n\n[RivetType]\npublic sealed record TaskItem(Guid Id, string Title, Priority Priority, Email Author);\n\n[RivetType]\npublic sealed record ErrorDto(string Code, string Message);\n```\n\n```typescript\n// Generated\nexport type Priority = \"Low\" | \"Medium\" | \"High\" | \"Critical\";\nexport type Email = string \u0026 { readonly __brand: \"Email\" };\nexport type TaskItem = { id: string; title: string; priority: Priority; author: Email };\nexport type ErrorDto = { code: string; message: string };\n```\n\n## Mark your controllers → get a typed client\n\n```csharp\n[RivetClient]\n[Route(\"api/tasks\")]\npublic sealed class TasksController : ControllerBase\n{\n    [HttpGet(\"{id:guid}\")]\n    [ProducesResponseType(typeof(ErrorDto), 404)]\n    public async Task\u003cActionResult\u003cTaskDetailDto\u003e\u003e Get(Guid id, CancellationToken ct) { ... }\n}\n```\n\n```typescript\n// Generated — discriminated union, narrowable by status\nexport type GetResult =\n  | { status: 200; data: TaskDetailDto; response: Response }\n  | { status: 404; data: ErrorDto; response: Response }\n  | { status: Exclude\u003cnumber, 200 | 404\u003e; data: unknown; response: Response };\n\nconst task = await tasks.get(id);                        // → TaskDetailDto (throws on error)\nconst result = await tasks.get(id, { unwrap: false });   // → GetResult (no throw)\n```\n\n## Or define contracts → get compile-time enforcement\n\n```csharp\n// Define the API surface — pure Rivet, no ASP.NET dependency\n[RivetContract]\npublic static class MembersContract\n{\n    public static readonly RouteDefinition\u003cList\u003cMemberDto\u003e\u003e List =\n        Define.Get\u003cList\u003cMemberDto\u003e\u003e(\"/api/members\")\n            .Summary(\"List all team members\");\n}\n\n// Implement it — compiler enforces the return type matches the contract\n[HttpGet]\npublic async Task\u003cIActionResult\u003e List(CancellationToken ct)\n    =\u003e (await MembersContract.List.Invoke(async () =\u003e\n    {\n        return await db.Members.ToListAsync(ct); // must return List\u003cMemberDto\u003e\n    })).ToActionResult();\n\n// Works with minimal APIs too — .Route avoids duplicating the route string\napp.MapGet(MembersContract.List.Route, async (AppDb db, CancellationToken ct) =\u003e\n    (await MembersContract.List.Invoke(async () =\u003e\n    {\n        return await db.Members.ToListAsync(ct);\n    })).ToResult());  // you write ToResult() once, same pattern as ToActionResult()\n```\n\n## Add `--compile` → runtime validation with Zod\n\n```bash\ndotnet rivet --project Api.csproj --output ./generated --compile\n```\n\nRivet emits [Zod 4](https://zod.dev) validators backed by `fromJSONSchema()` — a `schemas.ts` with standalone JSON Schema definitions and a `validators.ts` that wraps them:\n\n```typescript\n// schemas.ts — standalone JSON Schema, usable with any validator\nimport type { core } from \"zod\";\ntype JSONSchema = core.JSONSchema.JSONSchema;\n\nconst $defs: Record\u003cstring, JSONSchema\u003e = { /* all type definitions */ };\nexport const TaskItemSchema: JSONSchema = { \"$ref\": \"#/$defs/TaskItem\", \"$defs\": $defs };\n\n// validators.ts — cached Zod schemas from the JSON Schema definitions\nimport { fromJSONSchema, z } from \"zod\";\nimport { TaskItemSchema } from \"./schemas.js\";\n\nconst _assertTaskItem = fromJSONSchema(TaskItemSchema);\nexport const assertTaskItem = (data: unknown): TaskItem =\u003e _assertTaskItem.parse(data) as TaskItem;\n```\n\nEvery 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.\n\nYou can also emit just the schemas without validation wiring:\n\n```bash\ndotnet rivet --project Api.csproj --output ./generated --jsonschema\n```\n\nThis writes `schemas.ts` only — use it with `fromJSONSchema()`, ajv, or any JSON Schema consumer.\n\n## Import OpenAPI → get C# contracts\n\nAnother 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.\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"TaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\", \"format\": \"uuid\" },\n          \"title\": { \"type\": \"string\" },\n          \"priority\": { \"$ref\": \"#/components/schemas/Priority\" }\n        },\n        \"required\": [\"id\", \"title\", \"priority\"]\n      },\n      \"Priority\": {\n        \"type\": \"string\",\n        \"enum\": [\"low\", \"medium\", \"high\", \"critical\"]\n      }\n    }\n  },\n  \"paths\": {\n    \"/api/tasks\": {\n      \"get\": {\n        \"tags\": [\"Tasks\"],\n        \"summary\": \"List all tasks\",\n        \"responses\": {\n          \"200\": {\n            \"content\": { \"application/json\": { \"schema\": {\n              \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TaskDto\" }\n            } } }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n```bash\ndotnet rivet --from-openapi spec.json --namespace MyApp.Contracts --output ./src/\n```\n\n```csharp\n// Generated — sealed records, enums, typed contract with builder chain\npublic enum Priority { Low, Medium, High, Critical }\n\npublic sealed record TaskDto(Guid Id, string Title, Priority Priority);\n\n[RivetContract]\npublic static class TasksContract\n{\n    public static readonly RouteDefinition\u003cList\u003cTaskDto\u003e\u003e List =\n        Define.Get\u003cList\u003cTaskDto\u003e\u003e(\"/api/tasks\")\n            .Summary(\"List all tasks\");\n}\n```\n\nThe 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](https://maxanstey-meridian.github.io/rivet/guides/openapi-import#unsupported-content-markers) for details.\n\n## Check contract coverage\n\n```bash\ndotnet rivet --project Api.csproj --check\n```\n\n```\nwarning: [MissingImplementation] MembersContract.Remove: expected DELETE /api/members/{id}, got (none)\nwarning: [RouteMismatch] MembersContract.UpdateRole: expected /api/members/{id}/role, got /api/members/{id}/update-role\nCoverage: 2/4 endpoints covered, 1 mismatch(es), 1 missing.\n```\n\nVerifies that every contract endpoint has a matching handler implementation, with correct HTTP method and route. Useful in CI to catch missing or mismatched handlers.\n\n## List your routes\n\n```bash\ndotnet rivet --project Api.csproj --routes\n```\n\n```\n  Method  Route                      Handler\n  ──────  ─────────────────────────  ───────\n  GET     /api/members               members.list\n  POST    /api/members               members.invite\n  DELETE  /api/members/{id}          members.remove\n  PUT     /api/members/{id}/role     members.updateRole\n4 route(s).\n```\n\n## Documentation\n\nGuides, reference, and architecture at **[maxanstey-meridian.github.io/rivet](https://maxanstey-meridian.github.io/rivet)**.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxanstey-meridian%2Frivet","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaxanstey-meridian%2Frivet","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaxanstey-meridian%2Frivet/lists"}