diff --git a/openapi/frameworks/trpc.mdx b/openapi/frameworks/trpc.mdx
index f447c2f9..7ae0f976 100644
--- a/openapi/frameworks/trpc.mdx
+++ b/openapi/frameworks/trpc.mdx
@@ -1,173 +1,337 @@
---
-title: How To Generate an OpenAPI Spec With tRPC
-description: "How to use tRPC to create an OpenAPI spec and create an SDK for it with Speakeasy."
+title: How to generate OpenAPI with tRPC
+description: "Generate an OpenAPI document for a tRPC API and use it to create an SDK using Speakeasy."
---
import { Callout } from "@/mdx/components";
-# How to generate an OpenAPI/Swagger spec with tRPC
+# How to generate OpenAPI with tRPC
-In this tutorial, we'll explore how to generate an OpenAPI document for our [tRPC](https://trpc.io/) API, and then we'll use this document to create an SDK using Speakeasy.
+This tutorial explores how to add REST endpoints to [tRPC](https://trpc.io/)
+procedures, then turn all those convenient types into generated OpenAPI which
+can be used for all sorts of handy things like creating an SDK with Speakeasy.
-Here's what we'll cover:
+The guide covers:
-1. Adding `trpc-openapi` to a tRPC project.
-2. Generating an OpenAPI specification using `trpc-openapi`.
-3. Improving the generated OpenAPI specification for better downstream SDK generation.
-4. Using the Speakeasy CLI to create an SDK based on the generated OpenAPI specification.
-5. Using the Speakeasy OpenAPI extensions to improve created SDKs.
-6. Automating this process as part of a CI/CD pipeline.
+- Adding REST-like HTTP endpoints to a tRPC project and making OpenAPI v3.1
+ using [`trpc-to-openapi`](](https://github.com/mcampa/trpc-to-openapi)).
+- Improving the generated OpenAPI for better use across the API lifecycle and
+ specifically SDK generation.
+- Using the Speakeasy CLI to create an SDK based on the generated OpenAPI.
-
- If you want to follow along, you can use the [**tRPC Speakeasy Bar example
- repository**](https://github.com/speakeasy-api/speakeasy-trpc-example).
-
-
-## The SDK Creation Pipeline
+## Introduction
-tRPC does not natively export OpenAPI documents, but the [`trpc-openapi`](https://github.com/jlalmes/trpc-openapi/) package adds this functionality. We'll start this tutorial by adding `trpc-openapi` to a project, and then we'll add a script to generate an OpenAPI schema and save it as a file.
+tRPC does not natively export OpenAPI documents, but seeing as both tRPC and
+OpenAPI are focused on declaring types and operations it's not a huge task to
+turn one into the other.
-The quality of your OpenAPI specification will ultimately determine the quality of created SDKs and documentation, so we'll dive into ways you can improve the generated specification.
+The [`trpc-to-openapi`](https://github.com/mcampa/trpc-to-openapi) package does
+exactly this, adding full OpenAPI support to tRPC. Hang on though, isn't OpenAPI
+meant to describe REST/RESTish APIs, not tRPC specifically? Well absolutely, the
+`trpc-to-openapi` creates a REST bridge for the tRPC procedures, making them
+accessible to the wider public allowing for more potential integrations on the
+same API, then describe it nicely to allow for documentation making that
+integration even easier.
-With our new and improved OpenAPI specification in hand, we'll take a look at how to create SDKs using Speakeasy.
+The quality of the generated OpenAPI will ultimately determine the quality of
+created SDKs and documentation, so this guide covers ways to improve the that
+OpenAPI by leveraging all sorts of handy features in tRPC, OpenAPI, and the
+integrations between the two.
-Finally, we'll add this process to a CI/CD pipeline so that Speakeasy automatically creates fresh SDKs whenever your tRPC API changes in the future.
+Finally, this process is added to a CI/CD pipeline so that Speakeasy
+automatically creates fresh SDKs whenever the tRPC API changes in the future.
## Requirements
-To follow along with this tutorial, you will need:
+To follow along with this tutorial, the following are needed:
-- An existing tRPC app, or you can clone our example application.
- Some familiarity with tRPC.
-- [Node.js](https://nodejs.org/en/download) installed (we used Node v20.5.1).
-- The [Speakeasy CLI](/docs/speakeasy-reference/cli/getting-started) installed. You'll use the CLI to create the SDK once you have generated your OpenAPI spec.
+- [Node.js](https://nodejs.org/en/download) (Node 26.3.0 was used here).
+- [Speakeasy CLI](/docs/speakeasy-reference/cli/getting-started) (to create the SDK)
+- An existing tRPC app, or the [example application](https://github.com/speakeasy-api/examples) can be cloned.
+
+
+ Follow along with this guide using the sample code in the [Speakeasy examples
+ repository](https://github.com/speakeasy-api/examples/), with this code living
+ under frameworks-trpc. This sample code is based on a common OpenAPI
+ example: the [Train Travel API](https://github.com/bump-sh-examples/train-travel-api).
+
-## Supported OpenAPI Versions
+The specific versions will change over time, but these are the versions used in
+this guide and the accompanying sample code:
-Speakeasy supports OpenAPI v3 and v3.1. As of October 2023, `trpc-openapi` can generate schemas that adhere to the [OpenAPI v3.0.3 specification](https://spec.openapis.org/oas/v3.0.3).
+```json filename="package.json"
+{
+ "dependencies": {
+ "@trpc/client": "^11.18.0",
+ "@trpc/server": "^11.18.0",
+ "express": "^5.2.1",
+ "trpc-to-openapi": "^3.3.0",
+ "zod": "^4.0.0"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.6",
+ "tsx": "^4.23.1",
+ "typescript": "^5.2.2"
+ }
+}
+```
+
+## Supported OpenAPI versions
-This OpenAPI version is not a limitation, but it is important to keep the versions used in mind when debugging code generation. Refer to the OpenAPI Initiative for an overview of the [differences between OpenAPI 3.0 and 3.1.0](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0).
+Speakeasy supports all modern versions of OpenAPI from v3.0 to v3.2.
+`trpc-to-openapi` supports [OpenAPI v3.1
+](https://spec.openapis.org/oas/v3.1.0), so the tutorial examples use that.
-## Generate an OpenAPI/Swagger spec with tRPC
+Hopefully `trpc-to-openapi` will support v3.2 in the future, but it does not
+make a huge difference as lots of tools work just fine with either version.
-We'll use [`trpc-openapi`](https://github.com/jlalmes/trpc-openapi/) to create REST endpoints for our tRPC procedures and then create the OpenAPI spec that describes these endpoints.
+## Adding easy REST-like endpoints to tRPC procedures
-To generate an OpenAPI spec for tRPC, we'll use [trpc-openapi](https://github.com/jlalmes/trpc-openapi/) to create REST endpoints for our tRPC procedures, then create an OpenAPI document that describes these endpoints.
+The `trpc-to-openapi` library has two main features: adding REST/RESTish endpoints
+to tRPC procedures, and generating an OpenAPI document for these endpoints.
-### Add trpc-openapi to a Project
+Why would you want to add REST endpoints to tRPC procedures? For the same
+reason gRPC has the [gRPC Gateway](/openapi/frameworks/grpc-gateway) to turn
+services into REST endpoints: at first some teams pick something expecting
+it only to be used by them, but later they want to expose it to other teams or
+external developers. Adding REST endpoints to tRPC procedures allows APIs
+to be opened up to a wide variety of users who don't all want to learn tRPC.
-Install `trpc-openapi`:
+First step, install `trpc-to-openapi`:
```bash
-npm install trpc-openapi
+npm install trpc-to-openapi
```
Use `initTRPC.meta()` to create a new tRPC instance with OpenAPI support:
-```typescript filename="router.ts"
+```typescript filename="server/router.ts"
import { initTRPC } from "@trpc/server";
-import { OpenApiMeta } from "trpc-openapi";
+import { OpenApiMeta } from "trpc-to-openapi";
const t = initTRPC.meta().create();
```
-Add OpenAPI meta to a procedure by passing an `openapi` object to the `meta` function. This object contains the HTTP method and path for the generated REST endpoint.
+Add OpenAPI meta to a procedure by passing an `openapi` object to the `meta`
+function. This object contains the HTTP method and path for the generated REST
+endpoint.
-```typescript filename="router.ts"
+```typescript filename="server/router.ts"
import { initTRPC } from "@trpc/server";
-import { OpenApiMeta } from "trpc-openapi";
+import { OpenApiMeta } from "trpc-to-openapi";
import { z } from "zod";
const t = initTRPC.meta().create();
export const appRouter = t.router({
- findByProductCode: t.procedure
- .meta({ openapi: { method: "GET", path: "/find" } })
- .input(z.object({ code: z.string() }))
- .output(z.object({ drink: z.object({ name: z.string() }) }))
+ getStations: t.procedure
+ .meta({ openapi: { method: "GET", path: "/stations" } })
+ .input(z.object({ search: z.string().optional() }))
+ .output(z.object({ data: z.array(z.object({ name: z.string() })) }))
.query(async ({ input }) => {
- const drink = {
- name: "Old Fashioned",
- };
- return { drink: drink };
+ const stations = [
+ { name: "Berlin Hauptbahnhof" },
+ ];
+ return { data: stations };
}),
});
```
-Add a new script to generate an OpenAPI document based on the tRPC router:
+Mount both the tRPC middleware and the OpenAPI middleware in the server:
-```typescript filename="openapi.ts"
-import { generateOpenApiDocument } from "trpc-openapi";
+```typescript filename="server/index.ts"
+import { createExpressMiddleware } from "@trpc/server/adapters/express";
+import express from "express";
+import { createOpenApiExpressMiddleware } from "trpc-to-openapi";
+
+import { appRouter } from "./router";
+
+const app = express();
+
+app.use("/api/trpc", createExpressMiddleware({ router: appRouter }));
+app.use("/api", createOpenApiExpressMiddleware({ router: appRouter }));
+
+app.listen(3123);
+```
+
+Add a start script so the server can be run locally:
+
+```json filename="package.json"
+{
+ "scripts": {
+ "start": "tsx server/index.ts"
+ }
+}
+```
+
+Then start the server:
+
+```bash
+npm run start
+```
+
+Once the local server is running and the OpenAPI middleware is mounted at
+`/api`, the `getStations` procedure is also available as a REST-like endpoint.
+For example, this request hits the generated `GET /api/stations` route on the
+local server:
+
+```bash filename="Terminal"
+curl "http://localhost:3123/api/stations?search=Berlin"
+```
+
+The output should look something like this. Real output, from the code.
+
+```json
+{
+ "data": [
+ {
+ "id": "efdbb9d1-02c2-4bc3-afb7-6788d8782b1e",
+ "name": "Berlin Hauptbahnhof",
+ "address": "Invalidenstrasse 10557 Berlin, Germany",
+ "country_code": "DE",
+ "timezone": "Europe/Berlin"
+ }
+ ]
+}
+```
+
+So far this is nothing to do with OpenAPI really. It's more about adding HTTP
+bindings to tRPC so that a wider variety of clients can interact with it
+directly, and getting it ready for OpenAPI to pick that up and add more context.
+
+## Configure OpenAPI from source code
+
+With this handy new REST-like API setup with minimal HTTP mapping, we can now
+generate an OpenAPI document and export it to something like `openapi-spec.json`.
+Once this files exists, it can be used by a [massive ecosystem of API
+tooling](https://openapi.tools/) for everything from mock servers and contract
+testing to API gateways and SDK generation.
+
+OpenAPI requires a little bit more than just a list of HTTP methods and paths,
+so the next step is to add some basic information about the API to the OpenAPI
+document in `server/openapi.ts`:
+
+```typescript filename="server/openapi.ts"
+import { generateOpenApiDocument } from "trpc-to-openapi";
import { appRouter } from "./router";
export const openApiDocument = generateOpenApiDocument(appRouter, {
- title: "tRPC OpenAPI",
+ title: "Speakeasy tRPC Example",
version: "1.0.0",
- baseUrl: "http://localhost:3000",
+ baseUrl: "http://localhost:3123",
});
```
-Add a script to save the generated OpenAPI document to a file:
+This `server/openapi.ts` file can then be imported into a script to generate an OpenAPI
+document and save it to a file:
-```typescript filename="generateOpenApi.ts"
+```typescript filename="server/generateOpenApi.ts"
import { openApiDocument } from "./openapi";
console.log(JSON.stringify(openApiDocument, null, 2));
```
-Run the script to generate an OpenAPI document:
+Run the script to generate the OpenAPI document:
```bash
-ts-node generateOpenApi.ts > openapi-spec.json
+tsx server/generateOpenApi.ts
```
-Add this document to the `package.json` file as a script:
+This should output a whole load of JSON to the screen, starting with something
+like:
+
+```json
+{
+ "openapi": "3.1.0",
+ ...
+}
+```
+
+Staring directly into the void of JSON is not very useful, so let's make another handy
+script to save it to a file so other tools can work with it.
```json filename="package.json"
{
"scripts": {
- "generate-openapi": "ts-node generateOpenApi.ts > openapi-spec.json"
+ "generate-openapi": "tsx server/generateOpenApi.ts > openapi-spec.json"
+ }
+}
+```
+
+From now on, an OpenAPI document can be generated by running `npm run generate-openapi`.
+
+## Previewing the OpenAPI document
+
+The team over at [Scalar](https://scalar.com/) have built a fantastic CLI and
+documentation tool which can serve an OpenAPI document as a web app, using the
+popular Stripe-like "three column" API documentation format.
+
+```json
+{
+ "scripts": {
+ "generate-openapi": "tsx server/generateOpenApi.ts > openapi-spec.json",
+ "preview-openapi": "npm run generate-openapi && npx @scalar/cli document serve openapi-spec.json"
}
}
```
-From now on, we can generate an OpenAPI document by running `npm run generate-openapi`.
+Now run the following to generate and preview the latest OpenAPI:
+
+```bash filename="Terminal"
+npm run preview-openapi
+```
+
+
+
+The API routes are listed in the navigation pane on the left. Click
+**`/bookings` POST** to navigate to one of the Train Travel resource endpoints:
+
+
+
+Each section shows information about an API endpoint, such as its path
+parameters, request body, and responses.
-When we inspect the generated OpenAPI document, we can see that it contains a single endpoint for the `findByProductCode` procedure, but it's missing a lot of information.
+It's even possible to test the API directly from the Scalar UI by clicking the **Try it out** button.
-Let's see how we can improve this document.
+
-## How To Improve the OpenAPI Info Section
+## How to improve the OpenAPI info section
-The OpenAPI info section contains information about the API, such as the title, description, and version. If you use Speakeasy later, it will use this information to create documentation and SDKs.
+The OpenAPI info section contains information about the API, such as the title, description, and version. If Speakeasy is used later, it will use this information to create documentation and SDKs.
-The `GenerateOpenApiDocumentOptions` type from `trpc-openapi` allows us to add this information to our OpenAPI document:
+The `GenerateOpenApiDocumentOptions` type from `trpc-to-openapi` allows this information to be added to the OpenAPI document:
-```typescript filename="node_modules/trpc-openapi/dist/generator/index.d.ts"
-export type GenerateOpenApiDocumentOptions = {
+```typescript filename="node_modules/trpc-to-openapi/dist/generator/index.d.ts"
+export interface GenerateOpenApiDocumentOptions> {
title: string;
description?: string;
version: string;
+ contact?: OpenApiContactObject;
+ license?: OpenApiLicenseObject;
+ openApiVersion?: string;
baseUrl: string;
docsUrl?: string;
tags?: string[];
- securitySchemes?: OpenAPIV3.ComponentsObject["securitySchemes"];
-};
+ securitySchemes?: Record;
+ filter?: (ctx: { metadata: { openapi: NonNullable } & TMeta }) => boolean;
+ defs?: Record;
+}
```
-We can add this information to our `generateOpenApiDocument` call:
+This information can be added to the `generateOpenApiDocument` call:
```typescript filename="openapi.ts"
-import { generateOpenApiDocument } from "trpc-openapi";
+import { generateOpenApiDocument } from "trpc-to-openapi";
import { appRouter } from "./router";
export const openApiDocument = generateOpenApiDocument(appRouter, {
- title: "Speakeasy Bar API",
- description: "An API to order drinks from the Speakeasy Bar",
- version: "1.0.0",
- baseUrl: "http://localhost:3000",
+ title: "Train Travel API",
+ description: "API for finding and booking train trips across Europe.",
+ version: "1.2.1",
+ baseUrl: "http://localhost:3123/api",
docsUrl: "http://example.com/docs",
- tags: ["drinks"],
+ tags: ["Stations", "Trips", "Bookings", "Payments"],
});
```
@@ -175,20 +339,29 @@ Run `npm run generate-openapi` and see how this information is added to the Open
```json filename="openapi-spec.json"
{
- "openapi": "3.0.3",
+ "openapi": "3.1.0",
"info": {
- "title": "Speakeasy Bar API",
- "description": "An API to order drinks from the Speakeasy Bar",
- "version": "1.0.0"
+ "title": "Train Travel API",
+ "description": "API for finding and booking train trips across Europe.",
+ "version": "1.2.1"
},
"servers": [
{
- "url": "http://localhost:3000"
+ "url": "http://localhost:3123/api"
}
],
"tags": [
{
- "name": "drinks"
+ "name": "Stations"
+ },
+ {
+ "name": "Trips"
+ },
+ {
+ "name": "Bookings"
+ },
+ {
+ "name": "Payments"
}
],
"externalDocs": {
@@ -198,83 +371,113 @@ Run `npm run generate-openapi` and see how this information is added to the Open
}
```
-## Improving the OpenAPI Paths
+## Improving the OpenAPI paths
-We can improve our OpenAPI document by adding fields to the procedure's input and output schemas, and by adding examples, documentation, and metadata.
+With the generic information covered in the `intro` section, it's time to focus
+on documenting the procedures (or in OpenAPI: operations), which is achieved by
+adding more fields to the procedure's input and output schemas. The fields being
+added cover examples, documentation, and metadata.
-### Expanding the Procedure's Input and Output Schemas
+### Expanding the procedure's input and output schemas
-Let's create a `Drink` model and add a few field types to see how these are represented in the OpenAPI document.
+Let's create a `Station` model and add a few field types to see how these are
+represented in the OpenAPI document.
-Create a new file called `models.ts` and specify a `Drink` model using Zod:
+Create a new file called `shared/models.ts` and specify a `Station` model using Zod:
-```typescript filename="models.ts"
+```typescript filename="shared/models.ts"
import { z } from "zod";
-const DrinkType = z
- .enum(["NON_ALCOHOLIC", "BEER", "WINE", "SPIRIT", "OTHER"])
- .describe("The type of drink");
-type DrinkType = z.infer;
-
-export const ProductCode = z.string().describe("The product code of the drink");
-export type ProductCode = z.infer;
-
-export const DrinkSchema = z.object({
- name: z.string().describe("The name of the drink"),
- type: DrinkType,
- price: z.number().describe("The price of the drink"),
- stock: z.number().describe("The number of drinks in stock"),
- productCode: ProductCode,
- description: z.string().nullable().describe("A description of the drink"),
+export const StationSchema = z.object({
+ id: z.string().uuid().meta({
+ description: "Unique station ID",
+ example: "efdbb9d1-02c2-4bc3-afb7-6788d8782b1e",
+ }),
+ name: z.string().meta({
+ description: "Name of the station",
+ example: "Berlin Hauptbahnhof",
+ }),
+ address: z.string().meta({
+ description: "Street address of the station",
+ example: "Invalidenstrasse 10557 Berlin, Germany",
+ }),
+ country_code: z.string().meta({
+ description: "ISO 3166-1 alpha-2 country code",
+ example: "DE",
+ }),
+ timezone: z
+ .string()
+ .meta({
+ description: "IANA timezone of the station",
+ example: "Europe/Berlin",
+ })
+ .optional(),
});
-export type Drink = z.infer;
+export type Station = z.infer;
```
-Back in the `router.ts` file, import these models and update the procedure's input and output schemas. In the example app, we also added a mock database.
+Back in `server/router.ts`, import these models and update the procedure's input
+and output schemas. In the example app, a mock database is also added.
-```typescript filename="router.ts"
+```typescript filename="server/router.ts"
import { initTRPC } from "@trpc/server";
-import { OpenApiMeta } from "trpc-openapi";
+import { OpenApiMeta } from "trpc-to-openapi";
import { z } from "zod";
import { db } from "./db"; // Mock database
-import { DrinkSchema, ProductCode } from "./models";
+import { StationSchema } from "../shared/models";
const t = initTRPC.meta().create();
export const appRouter = t.router({
- findByProductCode: t.procedure
- .meta({ openapi: { method: "GET", path: "/find" } })
- .input(z.object({ code: ProductCode }))
- .output(z.object({ drink: DrinkSchema.optional() }))
+ getStations: t.procedure
+ .meta({ openapi: { method: "GET", path: "/stations" } })
+ .input(
+ z.object({
+ page: z.number().meta({
+ description: "Page number to return",
+ example: 1,
+ }).optional(),
+ search: z.string().meta({
+ description: "Filter stations by name or address",
+ example: "Berlin",
+ }).optional(),
+ country: z.string().meta({
+ description: "Filter stations by ISO 3166-1 alpha-2 country code",
+ example: "DE",
+ }).optional(),
+ })
+ )
+ .output(z.object({ data: z.array(StationSchema) }))
.query(async ({ input }) => {
- const drink = await db.drink.findByProductCode(input.code);
- return { drink: drink };
+ const stations = await db.station.findAll(input);
+ return { data: stations };
}),
});
```
-If we regenerate the OpenAPI document, we can see that the `Drink` model is now included in the document with all of its fields:
+After the OpenAPI document is regenerated, the `Station` model is included in
+the document with all of its fields:
```json filename="openapi-spec.json"
{
"paths": {
- "/find": {
+ "/stations": {
"get": {
- "operationId": "findByProductCode",
- "summary": "Find a drink by product code",
- "description": "Pass the product code of the drink to search for",
- "tags": ["drinks"],
+ "operationId": "getStations",
+ "summary": "Get a list of train stations",
+ "description": "Returns a paginated and searchable list of all train stations",
+ "tags": ["Stations"],
"parameters": [
{
- "name": "code",
+ "name": "search",
"in": "query",
- "required": true,
+ "required": false,
"schema": {
"type": "string"
},
- "description": "The product code of the drink",
- "example": "1234"
+ "description": "Filter stations by name or address",
+ "example": "Berlin"
}
],
"responses": {
@@ -285,71 +488,60 @@ If we regenerate the OpenAPI document, we can see that the `Drink` model is now
"schema": {
"type": "object",
"properties": {
- "drink": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "The name of the drink"
- },
- "type": {
- "type": "string",
- "enum": [
- "NON_ALCOHOLIC",
- "BEER",
- "WINE",
- "SPIRIT",
- "OTHER"
- ],
- "description": "The type of drink"
- },
- "price": {
- "type": "number",
- "description": "The price of the drink"
+ "data": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "description": "Unique station ID"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the station"
+ },
+ "address": {
+ "type": "string",
+ "description": "Street address of the station"
+ },
+ "country_code": {
+ "type": "string",
+ "description": "ISO 3166-1 alpha-2 country code"
+ },
+ "timezone": {
+ "type": "string",
+ "description": "IANA timezone of the station"
+ }
},
- "stock": {
- "type": "number",
- "description": "The number of drinks in stock"
- },
- "productCode": {
- "type": "string",
- "description": "The product code of the drink"
- },
- "description": {
- "type": "string",
- "nullable": true,
- "description": "A description of the drink"
- }
- },
- "required": [
- "name",
- "type",
- "price",
- "stock",
- "productCode",
- "description"
- ],
- "additionalProperties": false
+ "required": [
+ "id",
+ "name",
+ "address",
+ "country_code"
+ ],
+ "additionalProperties": false
+ }
}
},
"additionalProperties": false
},
"example": {
- "drink": {
- "name": "Beer",
- "type": "BEER",
- "price": 5,
- "stock": 10,
- "productCode": "1234",
- "description": "A nice cold beer"
- }
+ "data": [
+ {
+ "id": "efdbb9d1-02c2-4bc3-afb7-6788d8782b1e",
+ "name": "Berlin Hauptbahnhof",
+ "address": "Invalidenstrasse 10557 Berlin, Germany",
+ "country_code": "DE",
+ "timezone": "Europe/Berlin"
+ }
+ ]
}
}
}
},
- "default": {
- "$ref": "#/components/responses/error"
- }
+ // 400, 401, 403, 404, and 500 error responses omitted for brevity
}
}
}
@@ -360,209 +552,218 @@ If we regenerate the OpenAPI document, we can see that the `Drink` model is now
Speakeasy will use the descriptions in these fields to create documentation for the SDK.
-### OpenAPI Model Schemas in tRPC
+### OpenAPI model schemas in tRPC
-At Speakeasy, we recommend using OpenAPI model schemas so that schemas are reusable across multiple procedures. This simplifies SDK code creation, makes it easier to maintain your OpenAPI document, and provides a better developer experience for your users.
+At Speakeasy, OpenAPI model schemas are recommended so that schemas are reusable
+across multiple procedures. This simplifies SDK code creation, makes it easier
+to maintain the OpenAPI document, and provides a better developer experience for
+users.
-At the time of writing, there is an [open issue on the tRPC repository](https://github.com/trpc/trpc-openapi/issues/157) to add support for OpenAPI model schemas. Until this is implemented, we'll need to be content with the duplication of schemas across procedures.
+`trpc-to-openapi` supports this through the `defs` option passed to
+`generateOpenApiDocument`, which accepts a map of Zod schemas to include in the
+document's `components/schemas` section so they can be referenced with `$ref`
+instead of being duplicated inline.
-Under the hood, `trpc-openapi` uses the [Zod to Json Schema](https://www.npmjs.com/package/zod-to-json-schema) package, which supports custom strategies for generating references to schemas, but this functionality is not yet exposed in `trpc-openapi`.
+Under the hood, `trpc-to-openapi` uses the
+[`zod-openapi`](https://github.com/samchungy/zod-openapi) package to convert Zod
+schemas into OpenAPI documents.
-### Adding a Summary, Description, Examples, and Tags to a Procedure
+### Adding a summary, description, examples, and tags to a procedure
-We can enrich our OpenAPI document by adding a summary, description, examples, and tags to our procedure's metaobject.
+The OpenAPI document can be enriched by adding a summary, description, and tags
+to the procedure's metaobject.
-The `trpc-openapi` package uses these fields to generate the `summary`, `description`, `example`, and `tags` fields in the OpenAPI document.
+The `trpc-to-openapi` package uses these fields to generate the `summary`,
+`description`, and `tags` fields in the OpenAPI document.
-The `example` field is also used to add examples to the input schema.
+Unlike its predecessor, `trpc-to-openapi`'s `meta.openapi` object doesn't have
+an `example` field. Examples are added directly on the Zod schemas instead,
+using Zod v4's `.meta()` method (see the [Zod v4 guide](/openapi/frameworks/zod)
+for more on this), which `zod-openapi` reads when building the document.
-```typescript filename="router.ts"
+```typescript filename="server/router.ts"
import { initTRPC } from "@trpc/server";
-import { OpenApiMeta } from "trpc-openapi";
+import { OpenApiMeta } from "trpc-to-openapi";
import { z } from "zod";
import { db } from "./db";
-import { DrinkSchema, ProductCode } from "./models";
+import { StationSchema } from "../shared/models";
const t = initTRPC.meta().create();
export const appRouter = t.router({
- findByProductCode: t.procedure
+ getStations: t.procedure
.meta({
openapi: {
method: "GET",
- path: "/find",
- summary: "Find a drink by product code",
- description: "Pass the product code of the drink to search for",
- tags: ["drinks"],
- example: {
- request: {
- code: "1234",
- },
- response: {
- drink: {
- name: "Beer",
- type: "BEER",
- price: 5.0,
- stock: 10,
- productCode: "1234",
- description: "A nice cold beer",
- },
- },
- },
+ path: "/stations",
+ summary: "Get a list of train stations",
+ description: "Returns a paginated and searchable list of all train stations",
+ tags: ["Stations"],
},
})
- .input(z.object({ code: ProductCode }))
- .output(z.object({ drink: DrinkSchema.optional() }))
+ .input(
+ z.object({
+ search: z
+ .string()
+ .meta({
+ description: "Filter stations by name or address",
+ example: "Berlin",
+ })
+ .optional(),
+ })
+ )
+ .output(z.object({ data: z.array(StationSchema) }))
.query(async ({ input }) => {
- const drink = await db.drink.findByProductCode(input.code);
- return { drink: drink };
+ const stations = await db.station.findAll(input);
+ return { data: stations };
}),
});
```
-If we regenerate the OpenAPI document now, we can see that the `summary`, `description`, and `example` fields have been added to it.
+
+ Call `.meta()` before `.optional()`. `zod-openapi` reads metadata from the
+ underlying schema, so metadata attached after `.optional()` is dropped when
+ building the document.
+
-### Add Metadata to Tags
+After the OpenAPI document is regenerated, the `summary`, `description`, and
+`example` fields are added to it.
-The `trpc-openapi` package defines tags as a list of strings, but OpenAPI allows you to add metadata to tags. For example, you can add a description or a link to documentation to a tag.
+### Add metadata to tags
-Since `trpc-openapi` uses the [`openapi-types` package](https://www.npmjs.com/package/openapi-types) `OpenAPIV3.Document` type, which allows tags defined as a list of strings or a list of objects, we can extend our document to include tag objects with metadata even though `trpc-openapi` uses a list of strings.
+The `trpc-to-openapi` package defines tags as a list of strings, but OpenAPI
+allows metadata to be added to tags. For example, a description or a link to
+documentation can be added to a tag.
-Let's add a description to the `drinks` tag:
+Since `trpc-to-openapi` re-exports the
+[`openapi3-ts`](https://www.npmjs.com/package/openapi3-ts) package's
+`OpenAPIObject` type, which allows tags defined as a list of strings or a list
+of objects, the document can be extended to include tag objects with metadata
+even though `trpc-to-openapi` uses a list of strings.
-```typescript filename="openapi.ts"
-import { generateOpenApiDocument } from "trpc-openapi";
+Let's add a description to the `Stations` tag:
+
+```typescript filename="server/openapi.ts"
+import { generateOpenApiDocument } from "trpc-to-openapi";
import { appRouter } from "./router";
const openApiDocument = generateOpenApiDocument(appRouter, {
- title: "Speakeasy Bar API",
- description: "An API to order drinks from the Speakeasy Bar",
- version: "1.0.0",
- baseUrl: "http://localhost:3000",
+ title: "Train Travel API",
+ description: "API for finding and booking train trips across Europe.",
+ version: "1.2.1",
+ baseUrl: "http://localhost:3123",
docsUrl: "http://example.com/docs",
- tags: ["drinks"],
+ tags: ["Stations", "Trips", "Bookings", "Payments"],
});
// add metadata to tags
openApiDocument.tags = [
{
- name: "drinks",
- description: "Operations related to drinks",
+ name: "Stations",
+ description:
+ "Find and filter train stations across Europe, including location and local timezone.",
},
];
export { openApiDocument };
```
-Now we can see that the `description` field has been added to the `drinks` tag in the generated OpenAPI document:
+Now the `description` field is added to the `Stations` tag in the generated OpenAPI document:
```json filename="openapi-spec.json"
{
"tags": [
{
- "name": "drinks",
- "description": "Operations related to drinks"
+ "name": "Stations",
+ "description": "Find and filter train stations across Europe, including location and local timezone."
}
]
}
```
-### Add Speakeasy Extensions to Methods
-
-The OpenAPI vocabulary can sometimes be insufficient for your code creation needs. For these situations, Speakeasy provides a set of OpenAPI extensions. For example, you may want to give an SDK method a different name from the `OperationId`. You can use the Speakeasy `x-speakeasy-name-override` extension to do so.
-
-This time, unfortunately, the [`openapi-types` package](https://www.npmjs.com/package/openapi-types) `OperationObject` type does not allow for custom extensions, so we need to add the extension to the generated OpenAPI document manually.
+### Add Speakeasy extensions to methods
-Ideally, we would create a new type that extends `OpenAPIV3.Document` and adds the `x-speakeasy-name-override` extension to the `OperationObject` type, but for this tutorial, we'll keep it simple and add the extension by casting the path item to `any`.
+The OpenAPI vocabulary can sometimes be insufficient for code creation needs.
+For these situations, Speakeasy provides a set of OpenAPI extensions. For
+example, an SDK method might need a different name from the `OperationId`. The
+Speakeasy `x-speakeasy-name-override` extension can be used to do so.
-Let's add an `x-speakeasy-name-override` extension to the `findByProductCode` procedure.
+Since `trpc-to-openapi`'s `OpenAPIObject` and `OperationObject` types (from
+`openapi3-ts`) already extend an `ISpecificationExtension` interface that allows
+any `x-`-prefixed key, extensions can be assigned directly without any extra
+casting.
-First, we extend the `OpenAPIV3.OperationObject` and `OpenAPIV3.Document` types to add the `x-speakeasy-name-override` and other extensions:
+Let's add an `x-speakeasy-name-override` extension to the `getStations`
+procedure:
-```typescript filename="extended-types.ts"
-import { OpenAPIV3 } from "openapi-types";
-
-export type IExtensionName = `x-${string}`;
-export type IExtensionType = any;
-export type ISpecificationExtension = {
- [extensionName: IExtensionName]: IExtensionType;
-};
-
-export type ExtendedDocument = OpenAPIV3.Document & ISpecificationExtension;
-export type ExtendedOperationObject =
- OpenAPIV3.OperationObject;
-```
-
-Then we import our extended operation type and add the `x-speakeasy-name-override` extension to the `findByProductCode` procedure:
-
-```typescript filename="openapi.ts"
-import { generateOpenApiDocument } from "trpc-openapi";
-import { ExtendedOperationObject } from "./extended-types";
+```typescript filename="server/openapi.ts"
+import { generateOpenApiDocument } from "trpc-to-openapi";
import { appRouter } from "./router";
const openApiDocument = generateOpenApiDocument(appRouter, {
- title: "Speakeasy Bar API",
- description: "An API to order drinks from the Speakeasy Bar",
- version: "1.0.0",
- baseUrl: "http://localhost:3000",
+ title: "Train Travel API",
+ description: "API for finding and booking train trips across Europe.",
+ version: "1.2.1",
+ baseUrl: "http://localhost:3123",
docsUrl: "http://example.com/docs",
- tags: ["drinks"],
+ tags: ["Stations", "Trips", "Bookings", "Payments"],
});
// add metadata to tags
openApiDocument.tags = [
{
- name: "drinks",
- description: "Operations related to drinks",
+ name: "Stations",
+ description:
+ "Find and filter train stations across Europe, including location and local timezone.",
},
];
-if (
- openApiDocument.paths &&
- openApiDocument.paths["/find"] &&
- openApiDocument.paths["/find"].get
-) {
- (openApiDocument.paths["/find"].get as ExtendedOperationObject)[
- "x-speakeasy-name-override"
- ] = "searchDrink";
+const stationsOp = openApiDocument.paths?.["/stations"]?.get;
+if (stationsOp) {
+ stationsOp["x-speakeasy-name-override"] = "searchStations";
}
export { openApiDocument };
```
-## Add Retries to an SDK With `x-speakeasy-retries`
+## Add retries to an SDK with x-speakeasy-retries
-Speakeasy can create SDKs that follow custom rules for retrying failed requests. For instance, if your server fails to return a response within a specified time, you may want your users to retry their request without clobbering your server.
+Speakeasy can create SDKs that follow custom rules for retrying failed requests.
+For instance, if the server fails to return a response within a specified time,
+users may want to retry the request without clobbering the server.
-Add retries to Speakeasy-created SDKs by adding a top-level `x-speakeasy-retries` schema to your OpenAPI spec. You can also override the retry strategy per operation by adding `x-speakeasy-retries`.
+Add retries to Speakeasy-created SDKs by adding a top-level
+`x-speakeasy-retries` schema to the OpenAPI spec. The retry strategy can also be
+overridden per operation by adding `x-speakeasy-retries`.
-### Adding Global Retries and Retries per Endpoint
+### Adding global retries and retries per endpoint
-Let's add a global retry strategy to our OpenAPI document and override it for our `findByProductCode` procedure.
+Add a global retry strategy to the OpenAPI document and override it for the
+`getStations` procedure.
-```typescript filename="openapi.ts"
-import { generateOpenApiDocument } from "trpc-openapi";
-import { ExtendedDocument, ExtendedOperationObject } from "./extended-types";
+```typescript filename="server/openapi.ts"
+import { generateOpenApiDocument } from "trpc-to-openapi";
import { appRouter } from "./router";
const openApiDocument = generateOpenApiDocument(appRouter, {
- title: "Speakeasy Bar API",
- description: "An API to order drinks from the Speakeasy Bar",
- version: "1.0.0",
- baseUrl: "http://localhost:3000",
+ title: "Train Travel API",
+ description: "API for finding and booking train trips across Europe.",
+ version: "1.2.1",
+ baseUrl: "http://localhost:3123",
docsUrl: "http://example.com/docs",
- tags: ["drinks"],
+ tags: ["Stations", "Trips", "Bookings", "Payments"],
});
// add metadata to tags
openApiDocument.tags = [
{
- name: "drinks",
- description: "Operations related to drinks",
+ name: "Stations",
+ description:
+ "Find and filter train stations across Europe, including location and local timezone.",
},
];
-(openApiDocument as ExtendedDocument)["x-speakeasy-retries"] = {
+openApiDocument["x-speakeasy-retries"] = {
strategy: "backoff",
backoff: {
initialInterval: 500,
@@ -574,17 +775,10 @@ openApiDocument.tags = [
retryConnectionErrors: true,
};
-if (
- openApiDocument.paths &&
- openApiDocument.paths["/find"] &&
- openApiDocument.paths["/find"].get
-) {
- (openApiDocument.paths["/find"].get as ExtendedOperationObject)[
- "x-speakeasy-name-override"
- ] = "searchDrink";
- (openApiDocument.paths["/find"].get as ExtendedOperationObject)[
- "x-speakeasy-retries"
- ] = {
+const stationsOp = openApiDocument.paths?.["/stations"]?.get;
+if (stationsOp) {
+ stationsOp["x-speakeasy-name-override"] = "searchStations";
+ stationsOp["x-speakeasy-retries"] = {
strategy: "backoff",
backoff: {
initialInterval: 500,
@@ -600,7 +794,8 @@ if (
export { openApiDocument };
```
-Regenerate the OpenAPI document and you can see that the `x-speakeasy-retries` field has been added to the document.
+Regenerating the OpenAPI document shows that the `x-speakeasy-retries` field has
+been added to the document.
```json filename="openapi-spec.json"
{
@@ -621,28 +816,52 @@ Regenerate the OpenAPI document and you can see that the `x-speakeasy-retries` f
## Why Speakeasy and tRPC?
-tRPC's focus on type safety and developer experience sets it apart from other TypeScript API frameworks. By using TypeScript's type system along with a schema library like Zod, tRPC allows your server and client code to share types.
+One of tRPC's stated goals is to cut down on the need for codegen, but there is
+still a place for code generation in the tRPC ecosystem. While tRPC's [default
+client](https://trpc.io/docs/client/vanilla/setup) is useful for writing
+internal clients in a monorepo where a client can import the server's
+`AppRouter`, it does not make it easy to publish production-ready SDKs for use
+by internal and external developers. Nor does tRPC's type-safety extend to SDKs
+in languages other than TypeScript.
-One of tRPC's stated goals is to cut down on the need for codegen, but we believe there is still a place for code generation in the tRPC ecosystem. While tRPC's [default client](https://trpc.io/docs/client/vanilla/setup) is useful for writing internal clients in a monorepo where a client can import the server's `AppRouter`, it does not make it easy to publish production-ready SDKs for use by internal and external developers. Nor does tRPC's type-safety extend to SDKs in languages other than TypeScript.
+Speakeasy can help create type-safe, production-ready SDKs for a tRPC-base HTTP API in
+various languages, ensuring the released APIs will give users a great developer experience.
-Speakeasy can help you create type-safe, production-ready SDKs for your tRPC API in various languages so that you can focus on building your API, confident that your users will have a great developer experience.
+## Create an SDK based on OpenAPI
-## How To Create an SDK Based on the OpenAPI Spec
+After following the steps above, the OpenAPI document is ready to use as the basis
+for a new SDK.
-After following the steps above, we have an OpenAPI spec that is ready to use as the basis for a new SDK. Now we'll use Speakeasy to create an SDK.
-
-In the root directory of your project, run the following:
+In the root directory of the project, run the following:
```bash
speakeasy quickstart
```
-Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter `openapi-spec.json` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate.
+Follow the onscreen prompts to provide the necessary configuration details for
+the new SDK such as the name, schema location, and output path. Enter
+`openapi-spec.json` when prompted for the OpenAPI document location and select
+TypeScript when prompted for which language should be generated.
+
+## Add SDK creation to GitHub Actions
+
+The Speakeasy
+[`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action)
+repository provides workflows to integrate the Speakeasy CLI in a CI/CD pipeline
+so that client SDKs are recreated when the OpenAPI spec changes.
-## Add SDK Creation to GitHub Actions
+Speakeasy can be set up to automatically push a new branch to SDK repositories
+so that engineers can review and merge the SDK changes.
-The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows to integrate the Speakeasy CLI in your CI/CD pipeline so that your client SDKs are recreated when your OpenAPI spec changes.
+For an overview of how to set up automation for SDKs, see the Speakeasy [SDK
+Generation Action and Workflows](/docs/workflow-reference) documentation.
-You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes.
+## Alternative OpenAPI support in tRPC
-For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK Generation Action and Workflows](/docs/workflow-reference) documentation.
+tRPC also has an official OpenAPI package, documented at
+[https://trpc.io/docs/openapi](https://trpc.io/docs/openapi) and implemented via
+`@trpc/openapi`. This is a valid option for teams that want to stay close to the
+official tRPC ecosystem, but it is still in alpha and the package API may still
+change as it matures. For that reason, the examples in this guide use
+`trpc-to-openapi`, which is a more established workflow today while still
+producing OpenAPI that works well with Speakeasy.
diff --git a/public/assets/openapi/trpc/scalar-post.png b/public/assets/openapi/trpc/scalar-post.png
new file mode 100644
index 00000000..027a1a75
Binary files /dev/null and b/public/assets/openapi/trpc/scalar-post.png differ
diff --git a/public/assets/openapi/trpc/scalar-trpc-real-response.png b/public/assets/openapi/trpc/scalar-trpc-real-response.png
new file mode 100644
index 00000000..39b94538
Binary files /dev/null and b/public/assets/openapi/trpc/scalar-trpc-real-response.png differ
diff --git a/public/assets/openapi/trpc/scalar.png b/public/assets/openapi/trpc/scalar.png
new file mode 100644
index 00000000..54cec919
Binary files /dev/null and b/public/assets/openapi/trpc/scalar.png differ