使用 tRPC 上传文件
使用 multipart/form-data 和适当的解析器,将文件上传功能集成到 tRPC 变更中。
使用 tRPC 上传文件 是 CoddyKit 上的免费 tRPC End-to-End Type Safe APIs 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 tRPC End-to-End Type Safe APIs 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 tRPC End-to-End Type Safe APIs 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Intro to File Uploads
Welcome to handling file uploads with tRPC! Unlike simple JSON data, files (like images or documents) require a special approach because they are often binary data.
tRPC itself focuses on type-safe API definitions. It doesn't natively handle the low-level parsing of file uploads. Instead, it integrates seamlessly with the underlying HTTP server framework, like Express or Next.js, to manage this.
Understanding multipart/form-data
When you upload a file via a web form, the data is typically sent using the multipart/form-data encoding type. This is different from application/json, which is common for most tRPC requests.
multipart/form-data: Allows sending both text fields and binary files in a single request.application/json: Primarily for structured text data, not ideal for large binary files.
Your server needs a special 'parser' to correctly interpret multipart/form-data requests.
Server-side Setup: Express & Parsers
To integrate file uploads with tRPC, we'll use an Express server as our backend. Express is a popular Node.js framework that can easily host tRPC.
For parsing multipart/form-data, we'll use a middleware like express-fileupload. This middleware will process the incoming request and make the uploaded files accessible on the req object.
Basic Express Server with File Middleware
First, let's set up a basic Express server and integrate the express-fileupload middleware. This makes files available on the req object for later use.
Run npm install express express-fileupload in your project.
Try running this example:
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
const port = 3000;
// Enable file upload middleware
app.use(fileUpload());
app.get('/', (req, res) => {
res.send('Express server running!');
});
// Simple endpoint to show file access
app.post('/upload-test', (req, res) => {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('No files uploaded.');
}
// 'myFile' refers to the name attribute in the HTML input
let uploadedFile = req.files.myFile;
console.log('File received:', uploadedFile.name);
res.send(`File '${uploadedFile.name}' received.`);
});
app.listen(port, () => {
console.log(`Server on http://localhost:${port}`);
});Integrating tRPC into the Server
Now, let's add tRPC on top of our Express server. It's crucial that the express-fileupload middleware runs before the tRPC middleware. This ensures that req.files is populated before tRPC's createContext function is called.
Run npm install @trpc/server @trpc/express zod.
import express from 'express';
import fileUpload from 'express-fileupload';
import * as trpcExpress from '@trpc/express';
import { initTRPC } from '@trpc/server';
// Initialize tRPC (we'll define context and router later)
const t = initTRPC.context<any>().create();
const appRouter = t.router({}); // Empty router for now
// tRPC context function
function createContext({ req, res }: { req: express.Request, res: express.Response }) {
return { req, res, uploadedFiles: req.files }; // Files are here!
}
const app = express();
const port = 3000;
// 1. Enable file upload middleware FIRST
app.use(fileUpload());
// 2. Add tRPC middleware (after fileUpload)
app.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
})
);
app.get('/', (req, res) => {
res.send('tRPC file upload server running!');
});
app.listen(port, () => {
console.log(`Server on http://localhost:${port}`);
});Defining tRPC Context with File Access
As shown in the previous example, the express-fileupload middleware adds a files property to the express.Request object. By returning req.files in our createContext function, we make the uploaded files easily accessible within any tRPC procedure.
We've named this property uploadedFiles in our context for clarity.
import express from 'express';
import type { UploadedFile } from 'express-fileupload';
// Extend Express Request type (in a real project, this would be in a types file)
declare global {
namespace Express {
interface Request {
files?: { [key: string]: UploadedFile | UploadedFile[] };
}
}
}
export function createContext({
req,
res,
}: { req: express.Request; res: express.Response }) {
return {
req,
res,
// This makes the files available in `ctx.uploadedFiles`
uploadedFiles: req.files,
};
}Building the File Upload Mutation
Now we can define a tRPC mutation that uses the uploadedFiles from our context. We'll use Zod for basic input validation (e.g., a description for the file).
- Access
ctx.uploadedFiles. - Retrieve the specific file by its field name (e.g., 'myFile').
- Use the file's
mv()method to save it to disk.
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
import type { UploadedFile } from 'express-fileupload';
// Assuming 't' is initialized with context containing 'uploadedFiles'
const t = initTRPC.context<any>().create(); // Replace 'any' with your actual context type
export const appRouter = t.router({
uploadFile: t.procedure
.input(z.object({
description: z.string().optional(),
}))
.mutation(async ({ ctx, input }) => {
const { uploadedFiles } = ctx;
if (!uploadedFiles || Object.keys(uploadedFiles).length === 0) {
throw new Error('No files found in request.');
}
// Access the file by the name given in FormData (e.g., 'myFile')
const file = uploadedFiles.myFile as UploadedFile;
if (!file) {
throw new Error('File field "myFile" is missing.');
}
const savePath = `./uploads/${file.name}`; // Store temporarily
await file.mv(savePath); // Move file to destination
return {
fileName: file.name,
size: file.size,
description: input.description || 'No description',
message: 'File uploaded successfully!',
};
}),
});Client-side: Creating FormData
On the client, you'll typically use an HTML <input type="file"> element to allow users to select a file. To send this file, you'll create a FormData object.
FormData: A web API that lets you easily construct key-value pairs representing form fields and their values, including files.- Use
formData.append('fieldName', value)to add data. For files, the value is the File object itself.
<!-- Example HTML snippet -->
<input type="file" id="fileInput" />
<script>
const fileInput = document.getElementById('fileInput');
function prepareFormData() {
const file = fileInput.files[0];
if (!file) {
console.log('No file selected.');
return null;
}
const formData = new FormData();
// 'myFile' must match the field name expected by the server
formData.append('myFile', file);
formData.append('description', 'A document from the user.');
console.log('FormData prepared!');
return formData;
}
</script>Sending Files to the tRPC Endpoint
The standard tRPC client library isn't designed to send multipart/form-data directly for mutations. Instead, we typically use the native fetch API to send the FormData object to our tRPC endpoint.
The endpoint path will be /trpc/yourProcedureName, and the method will be POST.
Try running this example (ensure your server from Scene 5 & 7 is running):
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Upload File</title>
</head>
<body>
<input type="file" id="fileInput" />
<button onclick="uploadFile()">Upload</button>
<script>
async function uploadFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
alert('Please select a file first.');
return;
}
const formData = new FormData();
formData.append('myFile', file); // 'myFile' matches server's expected name
formData.append('description', 'A file from client!');
try {
const response = await fetch('http://localhost:3000/trpc/uploadFile', {
method: 'POST',
body: formData,
});
const result = await response.json();
if (response.ok) {
alert('Upload successful: ' + JSON.stringify(result));
} else {
alert('Upload failed: ' + JSON.stringify(result));
}
} catch (error) {
console.error('Error uploading file:', error);
alert('An error occurred during upload.');
}
}
</script>
</body>
</html>Quick Check
Which of the following statements are true about handling file uploads with tRPC?
Recap & Next Steps
You've learned how to integrate file upload functionality into your tRPC application!
- File uploads use
multipart/form-data, requiring server-side parsing. express-fileuploadmiddleware on an Express server handles this parsing.- By applying the file upload middleware before tRPC middleware,
req.filesbecomes available in your tRPC context. - Your tRPC mutation can then access and save the uploaded files.
- On the client, use
FormDataand the nativefetchAPI to send files to your tRPC endpoint.
Remember to consider robust file storage solutions (e.g., cloud storage like S3) and proper error handling for production applications.
常见问题解答
「使用 tRPC 上传文件」课时是免费的吗?
是的 — 「使用 tRPC 上传文件」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 tRPC End-to-End Type Safe APIs 课程的其余内容,请升级到 CoddyKit PRO。 tRPC End-to-End Type Safe APIs 课程共包含 4 节课。
「使用 tRPC 上传文件」这节课中我会学到什么?
使用 multipart/form-data 和适当的解析器,将文件上传功能集成到 tRPC 变更中。 你通过在浏览器中直接运行的动手代码来练习 tRPC End-to-End Type Safe APIs,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 tRPC End-to-End Type Safe APIs 需要有经验吗?
无需任何先前经验。CoddyKit 上的 tRPC End-to-End Type Safe APIs 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「使用 tRPC 上传文件」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 tRPC End-to-End Type Safe APIs 课中编写并运行代码吗?
能。每节 tRPC End-to-End Type Safe APIs 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 高效批量处理请求
- 实现乐观更新
- 使用 tRPC 上传文件
- 无限查询与基于游标的分页