RapidfileAPI
Sign in
  1. Upload API
  2. Next.js file upload

File uploads in Next.js (App Router)

A route handler that signs uploads, a client component with a progress bar, and a webhook route. Works on Vercel, Node or any host.

Why not just POST the file to a route handler?

You can read a FormData body in a Next.js route handler, but serverless functions usually limit request bodies to a few megabytes and time out on slow uploads. Every byte also costs function time. The robust approach is a direct upload: your route handler only signs the upload and the browser sends the file to storage. This guide wires that up with the Rapidfile Upload API in about forty lines.

1. Create an app and store the key

Open the dashboard, create an app and copy the secret key. It is shown once. Put it in .env.local (and in your host's environment settings for production). It must never be prefixed with NEXT_PUBLIC_, which would ship it to the browser.

.env.local
RAPIDFILE_SECRET_KEY=<your secret key>
RAPIDFILE_WEBHOOK_SECRET=<your webhook secret>

Then save the browser SDK into your project. It is a single MIT-licensed file with no dependencies:

terminal
curl -o lib/rapidfile-sdk.js https://api.rapidfile.cc/sdk.js

2. The route handler that signs uploads

This runs on the server. It checks the session, then asks Rapidfile for an upload URL and returns the JSON as is. Add your own rules for types and sizes here; this is the only place a user's request is checked.

app/api/upload/route.ts
// app/api/upload/route.ts: runs on your server, so the secret key stays private
import { auth } from '@/lib/auth'; // your own session check

export async function POST(req: Request) {
  const user = await auth(req);
  if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 });

  const { name, size, type } = await req.json();
  const res = await fetch('https://api.rapidfile.cc/v1/uploads', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name, size, type, metadata: { userId: user.id } }),
  });
  return Response.json(await res.json(), { status: res.status });
}

3. The client component

uploadFile() calls your route, then uploads directly. Files over 95 MB are sent as parallel parts with retries, so the same component handles a 20 KB avatar and a 4 GB video.

app/upload/page.tsx
// app/upload/page.tsx
'use client';
import { useState } from 'react';
import { uploadFile } from '@/lib/rapidfile-sdk'; // copy of https://api.rapidfile.cc/sdk.js

export default function UploadPage() {
  const [percent, setPercent] = useState(0);
  const [url, setUrl] = useState('');
  return (
    <div>
      <input
        type="file"
        onChange={async (e) => {
          const picked = e.target.files?.[0];
          if (!picked) return;
          const file = await uploadFile(picked, {
            getUpload: '/api/upload',
            onProgress: (p) => setPercent(Math.round(p.percent)),
          });
          setUrl(file.url);
        }}
      />
      <progress max={100} value={percent} />
      {url && <a href={url}>{url}</a>}
    </div>
  );
}

The returned file has an id and, for public files, a url on cdn.rapidfile.cc. Save both with a Server Action or another route, or rely on the webhook below.

4. Receive the webhook

In the dashboard, set the app's webhook URL to https://your-app.com/api/webhooks/rapidfile and copy the webhook secret. The route reads the raw body, checks the HMAC signature and handles the event. Put the verify function from the docs in lib/rapidfile-webhook.ts.

app/api/webhooks/rapidfile/route.ts
// app/api/webhooks/rapidfile/route.ts
import { verifyRapidfileSignature } from '@/lib/rapidfile-webhook'; // the Node function above

export async function POST(req: Request) {
  const raw = Buffer.from(await req.arrayBuffer());
  const ok = verifyRapidfileSignature(raw, req.headers.get('rapidfile-signature'), process.env.RAPIDFILE_WEBHOOK_SECRET!);
  if (!ok) return new Response('bad signature', { status: 400 });
  const event = JSON.parse(raw.toString('utf8'));
  // event.type: 'file.uploaded' | 'file.deleted', event.data: ApiFile
  return new Response('ok');
}

5. Private files in Server Components

For files only some users should see, upload with acl: 'private', then create a short-lived link while rendering:

app/documents/[id]/page.tsx
// app/documents/[id]/page.tsx (Server Component)
export default async function Doc({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const doc = await db.document.findUnique({ where: { id } }); // your data
  const res = await fetch(`https://api.rapidfile.cc/v1/files/${doc.fileId}/signed-url`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ expiresIn: 600 }),
    cache: 'no-store',
  });
  const { url } = await res.json();
  return <a href={url}>Download {doc.name}</a>;
}

Deploying

Nothing special is needed: the route handlers run on the Node.js or Edge runtime, and uploads never pass through them, so body size limits and function timeouts don't apply to the files. Remember to add both environment variables in your hosting provider.

Frequently asked questions

Does this work with the Pages Router?

Yes. Put the same fetch call in pages/api/upload.ts and return res.status(r.status).json(await r.json()). The client code is identical.

Can I import the SDK from the URL in Next.js?

Bundlers generally do not load modules from remote URLs, so save a copy of sdk.js in your project (one file, MIT). In plain HTML you can import it straight from https://api.rapidfile.cc/sdk.js.

Does it work on the Edge runtime?

Yes. The upload route only uses fetch and Response. The webhook example uses Buffer and node:crypto, so run that route on the Node.js runtime or port it to Web Crypto.

How big can uploads be?

Up to 512 MB per file on the free plan and up to 50 GB on Scale. The SDK switches to multipart automatically above 95 MB.

Where do I keep the secret key?

In server-only environment variables such as RAPIDFILE_SECRET_KEY in .env.local. Never use the NEXT_PUBLIC_ prefix for it.

Keep reading