Skip to main content

Uppy 4.0 is here: TypeScript rewrite, Google Photos, React hooks, and much more.

Next.js

Integration guide for Next.js featuring the dashboard, the tus uploader, transloadit, multipart uploads to a Next.js route, the Uppy UI components, and the React hooks.

tip

Uppy also has hooks and more React examples in the React docs.

Install

npm install @uppy/core @uppy/dashboard @uppy/react

Tus

Tus is an open protocol for resumable uploads built on HTTP. This means accidentally closing your tab or losing connection let’s you continue, for instance, your 10GB upload instead of starting all over.

Tus supports any language, any platform, and any network. It requires a client and server integration to work. We will be using tus Node.js.

Checkout the @uppy/tus docs for more information.

'use client';

import Uppy from '@uppy/core';
// For now, if you do not want to install UI components you
// are not using import from lib directly.
import Dashboard from '@uppy/react/lib/Dashboard';
import Tus from '@uppy/tus';
import { useState } from 'react';

import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';

function createUppy() {
return new Uppy().use(Tus, { endpoint: '/api/upload' });
}

export default function UppyDashboard() {
// Important: use an initializer function to prevent the state from recreating.
const [uppy] = useState(createUppy);

return <Dashboard theme="dark" uppy={uppy} />;
}

@tus/server does not not support the Next.js app router yet, which is based on the fetch Request API instead of http.IncomingMessage and http.ServerResponse.

Even if you are fully comitting to the app router, there is no downside to still having the pages router next to it for some Node.js style API routes.

Attach the tus server handler to a Next.js route handler in an optional catch-all route file.

/pages/api/upload/[[...file]].ts

import type { NextApiRequest, NextApiResponse } from 'next';
import { Server, Upload } from '@tus/server';
import { FileStore } from '@tus/file-store';

/**
* !Important. This will tell Next.js NOT Parse the body as tus requires
* @see https://nextjs.org/docs/api-routes/request-helpers
*/
export const config = {
api: {
bodyParser: false,
},
};

const tusServer = new Server({
// `path` needs to match the route declared by the next file router
path: '/api/upload',
datastore: new FileStore({ directory: './files' }),
});

export default function handler(req: NextApiRequest, res: NextApiResponse) {
return tusServer.handle(req, res);
}

Transloadit

note

Before continuing you should have a Transloadit account and a Template setup.

Transloadit’s strength is versatility. By doing video, audio, images, documents, and more, you only need one vendor for all your file processing needs. The @uppy/transloadit plugin directly uploads to Transloadit so you only have to worry about creating a template. It uses Tus under the hood so you don’t have to sacrifice reliable, resumable uploads for convenience.

When you go to production always make sure to set the signature. Not using Signature Authentication can be a security risk. Signature Authentication is a security measure that can prevent outsiders from tampering with your Assembly Instructions.

Generating a signature should be done on the server to avoid leaking secrets.

/app/api/transloadit/route.ts

import { NextResponse, NextRequest } from 'next/server';
import crypto from 'crypto';

function utcDateString(ms: number): string {
return new Date(ms)
.toISOString()
.replace(/-/g, '/')
.replace(/T/, ' ')
.replace(/\.\d+Z$/, '+00:00');
}

export async function POST(request: NextRequest) {
// expire 1 hour from now (this must be milliseconds)
const expires = utcDateString(Date.now() + 1 * 60 * 60 * 1000);
const authKey = process.env.TRANSLOADIT_KEY;
const authSecret = process.env.TRANSLOADIT_SECRET;
const templateId = process.env.TRANSLOADIT_TEMPLATE_ID;

// Typically, here you would also deny generating a signature for improper use
if (!authKey || !authSecret || !templateId) {
return NextResponse.json(
{ error: 'Missing Transloadit credentials' },
{ status: 500 },
);
}

const body = await request.json();
const params = JSON.stringify({
auth: {
key: authKey,
expires,
},
template_id: templateId,
fields: {
// This becomes available in your Template as `${fields.customValue}`
// and could be used to have a storage directory per user for example
customValue: body.customValue,
},
// your other params like notify_url, etc.
});

const signatureBytes = crypto
.createHmac('sha384', authSecret)
.update(Buffer.from(params, 'utf-8'));
// The final signature needs the hash name in front, so
// the hashing algorithm can be updated in a backwards-compatible
// way when old algorithms become insecure.
const signature = `sha384:${signatureBytes.digest('hex')}`;

return NextResponse.json({ expires, signature, params });
}

On the client we want to fetch the signature and params from the server. You may want to send values from React state along to your endpoint, for instance to add fields which you can use in your template as global variables.

// ...
function createUppy() {
const uppy = new Uppy();
uppy.use(Transloadit, {
async assemblyOptions() {
// You can send meta data along for use in your template.
// https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions
const { meta } = uppy.getState();
const body = JSON.stringify({ customValue: meta.customValue });
const res = await fetch('/transloadit-params', { method: 'POST', body });
return response.json();
},
});
return uppy;
}

function Component({ customValue }) {
// IMPORTANT: passing an initializer function to prevent the state from recreating.
const [uppy] = useState(createUppy);

useEffect(() => {
if (customValue) {
uppy.setOptions({ meta: { customValue } });
}
}, [uppy, customValue]);
}

HTTP uploads to your backend

If you want to handle uploads yourself, in Next.js or another server in any language, you can use @uppy/xhr-upload.

warning

The server-side examples are simplified for demonstration purposes and assume a regular file upload while @uppy/xhr-upload can also send FormData through the formData or bundle options.

import { NextRequest, NextResponse } from 'next/server';
import { writeFile } from 'node:fs/promises';
import path from 'node:path';

export const config = {
api: {
bodyParser: false,
},
};

export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get('file') as File | null;

if (!file) {
return NextResponse.json({ error: 'No file uploaded' }, { status: 400 });
}

const buffer = Buffer.from(await file.arrayBuffer());
const filename = file.name.replace(/\s/g, '-');
const filepath = path.join(process.cwd(), 'public', 'uploads', filename);

try {
await writeFile(filepath, buffer);
return NextResponse.json({
message: 'File uploaded successfully',
filename,
});
} catch (error) {
console.error('Error saving file:', error);
return NextResponse.json({ error: 'Error saving file' }, { status: 500 });
}
}
'use client';

import Uppy from '@uppy/core';
// For now, if you do not want to install UI components you
// are not using import from lib directly.
import Dashboard from '@uppy/react/lib/Dashboard';
import Xhr from '@uppy/xhr-upload';
import { useState } from 'react';

import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';

function createUppy() {
return new Uppy().use(Xhr, { endpoint: '/api/upload' });
}

export default function UppyDashboard() {
// Important: use an initializer function to prevent the state from recreating.
const [uppy] = useState(createUppy);

return <Dashboard theme="dark" uppy={uppy} />;
}

Next steps

  • Add client-side file restrictions.
  • Upload files together with other form fields with @uppy/form.
  • Use your language of choice instead of English.
  • Add an image editor for cropping and resizing images.
  • Download files from remote sources, such as Google Drive and Dropbox, with Companion.
  • Add Golden Retriever to save selected files in your browser cache, so that if the browser crashes, or the user accidentally closes the tab, Uppy can restore everything and continue uploading as if nothing happened.