By framework · Next.js

File upload API for Next.js, in one Server Action

App Router, Server Actions, API routes — same flat HTTP under the hood. Permanent CDN URLs, no S3 setup, key stays server-side. Works on Vercel hobby.

Get free API key 300 uploads / mo free · no credit card
'use server';
export async function uploadFile(formData) {
  const fd = new FormData();
  fd.append('file', formData.get('file'));
  const r = await fetch('https://filepost.dev/v1/upload', {
    method: 'POST',
    headers: { 'X-API-Key': process.env.FILEPOST_API_KEY },
    body: fd,
  });
  return (await r.json()).url; // permanent CDN URL
}

The full Server Action + form

App Router pattern. Server Action keeps the API key off the client. The form posts to it; the redirect or revalidate happens server-side.

// app/upload/actions.ts
'use server';

export async function uploadAction(formData: FormData) {
  const file = formData.get('file') as File | null;
  if (!file) return { error: 'No file' };

  const fd = new FormData();
  fd.append('file', file);

  const r = await fetch('https://filepost.dev/v1/upload', {
    method: 'POST',
    headers: { 'X-API-Key': process.env.FILEPOST_API_KEY! },
    body: fd,
    cache: 'no-store',
  });

  if (!r.ok) return { error: `Upload failed (${r.status})` };
  const data = await r.json();
  return { url: data.url as string };
}
// app/upload/page.tsx
import { uploadAction } from './actions';

export default function UploadPage() {
  return (
    <form action={uploadAction}>
      <input type="file" name="file" required />
      <button type="submit">Upload</button>
    </form>
  );
}

Two files. ~25 lines. The key never enters the browser bundle. Type-safe end-to-end. Plays nicely with progressive enhancement — works without JS too.

Pages Router / API route version

Same shape if you're on Pages Router or need a callable endpoint:

// pages/api/upload.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import formidable from 'formidable';
import fs from 'node:fs';

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

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const form = formidable();
  const [, files] = await form.parse(req);
  const file = Array.isArray(files.file) ? files.file[0] : files.file;
  if (!file) return res.status(400).json({ error: 'No file' });

  const fd = new FormData();
  fd.append('file', new Blob([fs.readFileSync(file.filepath)]), file.originalFilename!);

  const r = await fetch('https://filepost.dev/v1/upload', {
    method: 'POST',
    headers: { 'X-API-Key': process.env.FILEPOST_API_KEY! },
    body: fd,
  });

  res.status(r.status).json(await r.json());
}

FilePost vs uploadthing vs S3 + presigned URLs — for Next.js

What you do FilePost uploadthing S3 + presigned
Lines of Next.js code~25~30 (provider + file-router)~50 (signing endpoint + client)
Framework lockNoneStrong (Next.js / RSC)None
Server Action supportNativeYesYes (via wrapper)
API key stays server-sideYesYesYes
Permanent URLYesYesNo (object key)
Works on Vercel hobbyYesYesYes
next/image-compatible URLYes (add to remotePatterns)YesDepends on CloudFront
Free tier300 / mo, no card2 GB total, no card5 GB / 20k req
Paid plan$9 / mo flat$10 / mo~$0.023 / GB
Move to Remix laterNo code changeRewriteNo code change

The opinion

uploadthing is the most opinionated DX for Next.js — and that's the case for it and against it. The case for: typed file-router, hooks, drop-in provider, perfect Vercel integration. The case against: you're now coupled to Next.js. If you ever migrate to Remix, Astro, or a non-React stack, the upload layer comes apart.

FilePost is the boring middle path: a flat HTTP endpoint that doesn't know what framework you're using. Less magic, less coupling, and the same flat $9/mo wherever you go.

next.config.js for next/image

If you render uploaded images via <Image />, allowlist FilePost's CDN domain so Next.js will optimize it:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'filepost.dev', pathname: '/v1/file/**' },
    ],
  },
};

When NOT to use FilePost from Next.js

  • You want zero config and you're staying on Vercel forever — uploadthing is the better DX bet.
  • You need server-side image transforms (resize, crop, watermark) baked into the upload pipeline. Cloudinary or Uploadcare do that better.
  • You're proxying very large files (>4.5 MB) through a Vercel hobby function. Either upload directly from the browser (skip the function), upgrade to Vercel Pro, or move heavy upload routes to a different runtime.

Going further

Questions we actually get

Server Action or API route — which one should I use?

Server Actions are cleaner if you're on App Router and the upload is form-driven — no separate route file, type-safe, integrates with progressive enhancement. API routes are still the right call if you need streaming, custom headers, or to call the upload from a non-form context (mobile client, webhook). Both keep the API key server-side.

Is the FilePost API key safe in a Server Action?

Yes. Server Actions run on the server, so process.env.FILEPOST_API_KEY never reaches the browser bundle. Don't prefix the variable with NEXT_PUBLIC_ — that explicitly exposes it to the client.

Does FilePost work on Vercel's free hobby tier?

Yes. The pipeline is one outbound HTTPS call from your Next.js function — well within Vercel's hobby limits. The 4.5 MB request-body limit on Vercel functions does cap the file size you can proxy through; for larger uploads, POST directly from the browser or use a Vercel Pro plan.

App Router vs Pages Router?

Both work. App Router unlocks Server Actions; Pages Router uses /pages/api/*. The code samples above show both. The HTTP call to FilePost is identical either way.

Will this work with next/image after upload?

Yes. Add filepost.dev to next.config.jsimages.remotePatterns. The CDN URL FilePost returns is a regular HTTPS image URL that next/image can optimize and serve.

How does this compare to uploadthing?

uploadthing is more opinionated: a React provider, hooks, and a typed file-router DSL. FilePost is just an HTTP endpoint — you write the React code yourself, but you're not locked into Next.js. Pick uploadthing if you want maximum out-of-box DX inside Vercel; pick FilePost if you want flatter pricing and the option to move to any framework or runtime later.

One Server Action away.

Free tier covers most Next.js side projects forever. Paste the snippet, set FILEPOST_API_KEY in your env, ship.

Get free API key 300 uploads / mo · 50 MB max · no credit card