File Uploading
ts-rest supports multipart/form-data requests, this is useful for uploading files or working with FormData from a form.
Contract​
The contract implementation is the same as any other mutation, however, contentType
must be set to multipart/form-data
and the body
must be a FormData-compatible
object (one level deep, no weird nested structures!).
const c = initContract();
export const postsApi = c.router({
updatePostThumbnail: {
method: 'POST',
path: '/posts/:id/thumbnail',
contentType: 'multipart/form-data', // <- Only difference
body: c.type<{ thumbnail: File }>(), // <- Use File type in here
responses: {
200: z.object({
uploadedFile: z.object({
name: z.string(),
size: z.number(),
type: z.string(),
}),
}),
400: z.object({
message: z.string(),
}),
},
},
});
Client​
If your query utilizes multipart/form-data, ts-rest allows you to choose from FormData or a type safe object, the latter is recommended in most cases, just make sure you don't make a nested object.
// client.ts
const App = () => {
const [thumbnail, setThumbnail] = React.useState<File | null>(null);
return (
<div>
<input
multiple={false}
type="file"
onChange={(e) => setThumbnail(e.target.files?.[0] || null)}
/>
<button
onClick={() => {
if (file) {
apiClient.uploadFile({
body: {
thumbnail: file, // <- typed body with "File" type
},
});
}
}}
>
Upload
</button>
</div>
);
};
Server - Express​
With Express it is recommend to use the multer
package to handle the multipart/form-data requests.
ts-rest offers some nice types to help with this, however, we're leaving this up to you to implement with middleware outside of ts-rest.
file
is typed asunknown
<- BYO middlewarefiles
is typed asunknown
<- BYO middlewarebody
has had anyFile
types removed (so other types are still there)
const s = initServer();
const postsRouter = s.router(postsApi, {
updatePostThumbnail: async ({ file, files, body }) => {
const thumbnail = file as Express.Multer.File;
return {
status: 200,
body: {
message: `File ${thumbnail.originalname} successfully!`,
},
};
},
});
const app = express();
app.use(cors());
// File upload
app.post(postsApi.updatePostThumbnail.path, upload.single('thumbnail'));
Server - Nest​
With Nest this is a pretty simple implementation, due to the extensible Decorator driven approach of Nest, you're able to utilize your favourite multipart/form-data middleware, in this case we're following https://docs.nestjs.com/techniques/file-upload from Nest.
body
has had anyFile
types removed (so other types are still there)
// nest
@Controller()
export class AppController implements NestControllerInterface<typeof c> {
@TsRest(s.route.updateUserAvatar)
@UseInterceptors(FileInterceptor('avatar'))
async updateUserAvatar(
@TsRestRequest() { params: { id } }: RequestShapes['updateUserAvatar'],
@UploadedFile() avatar: Express.Multer.File
) {
return {
status: 200 as const,
body: {
message: `Updated user ${id}'s avatar with ${avatar.originalname}`,
},
};
}
}