Platform

Open Source

Building Documenso SDK and API v2

Building Documenso SDK and API v2

Jan 31, 2025

Documenso Typescript SDK
Documenso Typescript SDK

At the beginning of 2024, we decided to build a public API to provide users a way to integrate Documenso into their platforms, and custom workflows.

When we implemented v1 of the API, we decided to decouple the public API from the internal one, which provided us more flexibility and control. You can read more on the v1 architecture in the “Building the Documenso Public API - The Why and How” article.

Within months of release, the challenges of maintaining both APIs became apparent. Every change required implementation across both APIs, along with duplicate testing efforts. Logging, debugging, and general maintenance became increasingly complex.

As these challenges compounded, we found ourselves gradually neglecting to keep the public API up to date with the latest changes.

Rethinking the API, and our direction

As time went on we began exploring ideas on how to move forward with the APIs.

We asked ourselves - why not build a singular API that both our application and the public can consume? It’s something we considered when building v1, but decided against it due to the desire for more flexibility and control.

A core API would allow us to:

  • Focus our resources on building a single API

  • Immediately ship features to both the app and public API

  • Improve test coverage

  • Handle logging, debugging and maintenance in one location

To do that, we had to decide whether to go all in on ts-rest or tRPC as the backbone of our core API.

After careful consideration, we decided to go with tRPC because:

  1. Generating an OpenAPI spec was a lot easier using tRPC (although less flexible)

  2. Working with tRPC is a lot easier for developers

  3. Our whole API is already in tRPC, so we wouldn't need to port anything

SDKs and Better Documentation

The next step after deciding to build the v2 of the API was to provide a better experience for our users when using the API.

To do that, we chose Speakeasy to build SDKs, and Scalar for a better API documentation with interactive examples.

Building v2 API

Preparation

Prior to starting the work on v2 API, we had some goals we wanted to achieve to provide a better experience for developers and API consumers

  • Improve type safety

  • Consistent responses

  • Sensible naming

To achieve this, we refactored our database and implemented these Prisma generators to provide the required types and schemas for our database:

Combining these generators allows us to export Zod schemas directly from our Prisma models, which in turn enables us to have strongly-typed responses for the API.

model Document {
  externalId     String?    /// @zod.string.describe("A custom external...")
  userId         Int        /// @zod.number.describe("The ID of the...")
  authOptions    Json?      /// [DocumentAuthOptions] @zod.custom.use(...)
  ...
}

Implementation

To build a REST API from tRPC, we utilised an open source package called trpc-to-openapi which allows us to:

  • Create a REST API endpoint

  • Generate an OpenAPI specification, which we will use to generate SDKs

Currently the v2 API is still in beta, so the structure and code will be prone to changes. Our structure is very similar to how tRPC apps are normally created:

  1. Router - Handles combining all the routes

  2. Route - Handles the request, response and endpoint

  3. Implementation - Handles the actual code required for the event

Router

// File: packages/trpc/router/document.ts

export const documentRouter = router({
  findDocuments: findDocumentRoute,
  getDocument: getDocumentRoute,
  createDocument: createDocumentRoute,
  deleteDocument: deleteDocumentRoute,
})

Route

Each route will manage the OpenAPI specification and the request/response schemas.

This allows us to have a clear divide between the API request and implementation, which will be discussed next.

// File: packages/trpc/router/routes/documents/get-document.ts

export const ZGetDocumentRequestSchema = z.object({
  documentId: z.string(),
});

export const ZGetDocumentResponseSchema = ZDocumentSchema;

export const getDocumentRoute = authenticatedProcedure
  .meta({
    openapi: {
      method: 'GET',
      path: '/document/{documentId}',
      summary: 'Get document',
      description: 'Get a document by ID',
      tags: ['Documents'],
    },
  })
  .input(ZGetDocumentRequestSchema)
  .output(ZGetDocumentResponseSchema)
  .query(async ({ input, ctx }) => {
    // Any specfic logic required to map the request to the implementation.

    // Implementation will go here.
    return getDocument();
  });

Implementation

By decoupling the implementation from the actual request, it allows us to reuse code between different events. It can be more loose and flexible since it does not need to directly adhere to the API request.

Doing this allows us to extend and reuse this piece of code where needed, such as during server-side render or middleware callbacks.

// File: packages/lib/server-only/document/get-document.ts
export const getDocument = () => {
  // Implementation.
}

SDK

By using trpc-to-openapi to generate an OpenAPI specification, we can use 3rd party services to generate an SDK for us.

There are multiple ways to generate an SDK based on OpenAPI, we investigated the following solutions:

In the end, we decided to go with Speakeasy, since it seemed to have the best compatibility and documentation with our solution.

The resulting SDK is fully type-safe and easy to use.

import { Documenso } from "@documenso/sdk-typescript";

const documenso = new Documenso({
  apiKey: "<API_KEY>",
});

await documenso.documents.createV0({
  title: "Hello World!",
});

What's next?

The v2 API is currently in beta, and we're working with the community to make it better and get it ready for the stable release. Give v2 a go and let us know if you have any feedback or suggestions.

Resources:

If you want to have a more direct discussion, please join our Discord server.

Documenso

© 2024 Documenso, Inc. All rights reserved.

Documenso

© 2024 Documenso, Inc. All rights reserved.

Documenso

© 2024 Documenso, Inc. All rights reserved.