Better File Upload/Download operations in oRPC thanks to builtin integration of TUS · Issue #482 · middleapi/orpc · GitHub
Skip to content

Better File Upload/Download operations in oRPC thanks to builtin integration of TUS #482

Open

Description

@theoludwig

This is just an idea; I haven't thought about how it will work or what the API will look like yet.

As told in the documentation:

For files larger than 10MB, it is recommended to use a dedicated solution for performance and reliability.

From my understanding, it is not recommended for "big" files, because under the hood, it encodes/decodes in base64 (haven't tested it)?
Even, file upload is much more than only 1 API request, it's not the same thing as basic JSON api call. Performance and resumable file uploads are crucial + good UX client side to show how much kb has been uploaded and much need to be uploaded still etc.

tus - Open Protocol for Resumable File Uploads makes file uploads more reliable and performant for any size of files.

While, we can already use @tus/server in combination with oRPC, I believe there might be a better way that keep type-safety, zod goodness and simplicity while preserving tus goodness too.

Would be great to define oRPC route as "tus route" and then call them with the RPCClient like normal routes with more features like tus-js-client, for example:

const handleUpload = async (file: File): Promise<void> => {
  const upload = new tus.Upload(file, {
    retryDelays: [0, 1_000, 3_000, 5_000],
    metadata: {
      filename: file.name,
      mimetype: file.type
    },
    onError: (error) => {
      console.error(error)
    },
    onProgress: (bytesUploaded, bytesTotal) =>{
      const percentage = (bytesUploaded / bytesTotal * 100).toFixed(2)
      console.log(bytesUploaded, bytesTotal, percentage + "%")
    },
    onSuccess: () => {
    console.log("Download %s from %s", upload.file.name, upload.url)
    }
  })
  const [previousUpload] = await upload.findPreviousUploads()
  if (previousUpload != null) {
    upload.resumeFromPreviousUpload(previousUpload)
  }
  upload.start()
}

The good thing, is that we can have a onProgress and regularly tell the user, how much bytes uploaded, how much still needed, percentage.

Also we have out of the box, resumability so for example, if the internet connection of the user is bad, and is interrupted, they can try to upload again the file, without the need of uploading the file from scratch, especially useful for big files, or really bad connection, like mobile network.

We could have a way to do it, with oRPC, the same type-safety, and APIs to define middleware.

Activity

  1. dinwwwh commented on May 10, 2025

    @dinwwwh
    Member
    1. We don’t use Base64 for file uploads/downloads in oRPC. Instead, we transmit raw binary data + optionally combined with multipart/form‑data when needed (it's good as standard file upload/download - only missing dedicated features for large file handling)
    2. This feature could be implemented as a plugin. I'll make a note of it and evaluate the approach soon.
  2. franciscohermida commented on May 11, 2025

    @franciscohermida

    have been spending time figuring out the best way to handle files with orpc as well or if I just have a separate handlers

  3. theoludwig commented on May 11, 2025

    @theoludwig
    Author

    Meanwhile, I think, it would be great to elaborate in the documentation to why "it is not recommended", is it because it's doing 1 HTTP request for the whole file? Also, until there is a better solution in oRPC builtin, maybe the documentation should link to better solution like TUS.

  4. tresorama commented on May 29, 2025

    @tresorama
    Contributor

    @theoludwig How tus handle multiple file upload?
    One request per file ?

  5. theoludwig commented on May 29, 2025

    @theoludwig
    Author

    @theoludwig How tus handle multiple file upload? One request per file ?

    @tresorama TUS, send multiple requests based on the file size. Multiple requests can be sent even for a single file.

  6. tresorama commented on May 29, 2025

    @tresorama
    Contributor

    Thanks , so is not possible to have one endpoint for a form that can send both json data and files?
    Tus endpoints are files only ?

  7. JSteunou commented on Jun 2, 2025

    @JSteunou

    This is a great idea! Already using TUS with uppy for the front-end in my tRPC / fastify stack and loving it. Not difficult to setup with fastify but this would provide a very nice file upload solution out of the box with oRPC if it could be done.

  8. dinwwwh commented on Jun 26, 2025

    @dinwwwh
    Member
  9. francisco-barros-codes commented on Jul 22, 2026

    @francisco-barros-codes

    I'm trying to understand how file uploads are represented in the generated OpenAPI spec.

    I have the following API contract defined with Zod:

    const CreateSourceInputBodySchema = z.object({
      content: z.file().max(MAX_DIRECT_UPLOAD_BYTES).meta({
        description:
          "The artifact file to store. Capped at 10MB; use POST /sources/upload for larger artifacts.",
        format: "binary",
      }),
      name: z.string().trim().nonempty().max(256).optional().meta({
        description:
          "Human-friendly source name. Defaults to the uploaded file name when omitted.",
      }),
      origin: z.string().trim().nonempty().max(64).meta({
        description:
          'Free-form provenance label for where the artifact came from. Normalized to lowercase kebab-case on write (e.g. "Plaude AI" → "plaude-ai").',
      }),
      type: SourceTypeSchema.meta({
        description: "The kind of artifact the source holds",
      }),
    });

    My server implements this contract and expects a multipart/form-data request:

    upload: implementer.upload.handler(
      async ({ context, input, errors }): Promise<UploadSourceOutput> => {
        const result = await initSourceUploadUsecase({
          contentType: input.body.content_type,
          filename: input.body.filename,
          hash: input.body.hash,
          name: input.body.name,
          organizationId: context.organizationId,
          origin: input.body.origin,
          type: input.body.type,
        });
      }
    );

    Everything works correctly at runtime. However, when I generate an OpenAPI spec with the oRPC OpenAPI plugin and import the resulting spec.json into clients such as Postman or Scalar, the endpoint is not recognized as accepting multipart/form-data, and the content field is not treated as a file upload. It's treated as a string input as you described above. I want the default to be multipart/form-data.

    Am I missing additional metadata or configuration that's required for the OpenAPI generator to emit a multipart request body? Or is z.file() currently not translated into the appropriate OpenAPI schema (type: string, format: binary under multipart/form-data)?

    What is the recommended way to define this contract so that OpenAPI clients correctly infer that the endpoint accepts file uploads?

  10. dinwwwh commented on Jul 22, 2026

    @dinwwwh
    Member

    @JSteunou Could you point to the exact part of the generated OpenAPI spec that's causing the issue, and explain what you expect instead? I can't control how third-party tools interpret the spec. My focus is on generating a valid OpenAPI 3.1 specification. Personally, I believe this is a limitation of the tool you're using.

    (Please open a new issue for it)

  11. dinwwwh commented on Aug 15, 2026

    @dinwwwh
    Member

    In v2 we can use import { TmpFileUploadHandlerPlugin } from '@orpc/node' to increase upload ability!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions