123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- ---
- slug: /nextjs
- ---
- import Tabs from '@theme/Tabs';
- import TabItem from '@theme/TabItem';
- # Next.js
- Integration guide for [Next.js][] featuring the [dashboard](/docs/dashboard),
- the [tus](/docs/tus) uploader, [transloadit](/docs/transloadit), multipart
- uploads to a Next.js route, the Uppy UI components, and the
- [React hooks](/docs/react).
- :::tip
- Uppy also has hooks and more React examples in the [React docs](/docs/react).
- :::
- ## Install
- <Tabs>
- <TabItem value="npm" label="NPM" default>
- ```shell
- npm install @uppy/core @uppy/dashboard @uppy/react
- ```
- </TabItem>
- <TabItem value="yarn" label="Yarn">
- ```shell
- yarn add @uppy/core @uppy/dashboard @uppy/react
- ```
- </TabItem>
- </Tabs>
- ## Tus
- [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](/docs/tus) for more information.
- ```tsx
- '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](https://nextjs.org/docs/pages/building-your-application/routing/dynamic-routes#optional-catch-all-routes).
- `/pages/api/upload/[[...file]].ts`
- ```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](https://transloadit.com)
- account and a
- [Template](https://transloadit.com/docs/getting-started/my-first-app/) 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][transloadit-services]. The [`@uppy/transloadit`](/docs/transloadit)
- plugin directly uploads to Transloadit so you only have to worry about creating
- a [template][transloadit-concepts]. It uses
- [Tus](#i-want-reliable-resumable-uploads) 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](https://transloadit.com/docs/topics/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.
- <Tabs>
- <TabItem value="app" label="App Router" default>
- `/app/api/transloadit/route.ts`
- ```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 });
- }
- ```
- </TabItem>
- <TabItem value="pages" label="Pages Router">
- `/pages/api/transloadit/params.ts`
- ```ts
- import type { NextApiRequest, NextApiResponse } from 'next';
- import crypto from 'node:crypto';
- function utcDateString(ms: number): string {
- return new Date(ms)
- .toISOString()
- .replace(/-/g, '/')
- .replace(/T/, ' ')
- .replace(/\.\d+Z$/, '+00:00');
- }
- export default function handler(req: NextApiRequest, res: NextApiResponse) {
- // Typically, here you would also deny generating a signature for improper use
- if (req.method !== 'POST') {
- return res.status(405).json({ error: 'Method Not Allowed' });
- }
- // 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;
- if (!authKey || !authSecret || !templateId) {
- return res.status(500).json({ error: 'Missing Transloadit credentials' });
- }
- 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: req.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')}`;
- res.status(200).json({ expires, signature, params });
- }
- ```
- </TabItem>
- </Tabs>
- 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`](https://transloadit.com/docs/topics/assembly-variables/) which you
- can use in your template as global variables.
- ```js
- // ...
- 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`](/docs/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.
- :::
- <Tabs>
- <TabItem value="app" label="App Router" default>
- ```ts
- 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 });
- }
- }
- ```
- </TabItem>
- <TabItem value="pages" label="Pages Router">
- ```ts
- import type { NextApiRequest, NextApiResponse } from 'next';
- import { createWriteStream } from 'fs';
- import { pipeline } from 'stream/promises';
- import path from 'path';
- export const config = {
- api: {
- bodyParser: false,
- },
- };
- export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse,
- ) {
- if (req.method !== 'POST') {
- return res.status(405).json({ error: 'Method Not Allowed' });
- }
- try {
- const filename = `file-${Date.now()}.txt`;
- const filepath = path.join(process.cwd(), 'public', 'uploads', filename);
- const writeStream = createWriteStream(filepath);
- await pipeline(req, writeStream);
- res.status(200).json({ message: 'File uploaded successfully', filename });
- } catch (error) {
- console.error('Error saving file:', error);
- res.status(500).json({ error: 'Error saving file' });
- }
- }
- ```
- </TabItem>
- </Tabs>
- ```tsx
- '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](/docs/uppy/#restrictions).
- - Upload files together with other form fields with [`@uppy/form`](/docs/form).
- - Use your [language of choice](/docs/locales) instead of English.
- - Add an [image editor](docs/image-editor) for cropping and resizing images.
- - Download files from remote sources, such as [Google Drive](docs/google-drive)
- and [Dropbox](docs/dropbox), with [Companion](/docs/companion).
- - Add [Golden Retriever](/docs/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.
- [transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/
- [transloadit-services]: https://transloadit.com/services/
- [Next.js]: https://nextjs.org/
- [tus]: https://tus.io/
- [tus Node.js]: https://github.com/tus/tus-node-server
- [`@tus/server`]:
- https://github.com/tus/tus-node-server/tree/main/packages/server
|