Skip to main content

File Uploading

ts-rest supports multipart/form-data requests, this is useful for uploading files or working with FormData from a form.


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(),


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 (
onChange={(e) => setThumbnail([0] || null)}
onClick={() => {
if (file) {
body: {
thumbnail: file, // <- typed body with "File" type

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 as unknown <- BYO middleware
  • files is typed as unknown <- BYO middleware
  • body has had any File 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();


// File upload, 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 you're favourite multipart/form-data middleware, in this case we're following from Nest.

  • body has had any File types removed (so other types are still there)
// nest
export class AppController implements NestControllerInterface<typeof c> {
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}`,