javascript 44 lines · 9 steps

Streaming file uploads in a Next.js route

A POST route handler validates an uploaded file, then streams it to disk with a randomized name instead of buffering it in memory.

Explained by highlit
1import { NextResponse } from 'next/server';
2import { createWriteStream } from 'node:fs';
3import { mkdir } from 'node:fs/promises';
4import { pipeline } from 'node:stream/promises';
5import { Readable } from 'node:stream';
6import { randomUUID } from 'node:crypto';
7import path from 'node:path';
8 
9const UPLOAD_DIR = path.join(process.cwd(), 'uploads');
10const MAX_BYTES = 10 * 1024 * 1024;
11const ALLOWED = new Set(['image/png', 'image/jpeg', 'application/pdf']);
12 
13export async function POST(request) {
14 const formData = await request.formData();
15 const file = formData.get('file');
16 
17 if (!(file instanceof File)) {
18 return NextResponse.json({ error: 'No file provided' }, { status: 400 });
19 }
20 if (!ALLOWED.has(file.type)) {
21 return NextResponse.json({ error: `Unsupported type: ${file.type}` }, { status: 415 });
22 }
23 if (file.size > MAX_BYTES) {
24 return NextResponse.json({ error: 'File exceeds 10MB limit' }, { status: 413 });
25 }
26 
27 await mkdir(UPLOAD_DIR, { recursive: true });
28 
29 const ext = path.extname(file.name).slice(0, 10) || '';
30 const storedName = `${randomUUID()}${ext}`;
31 const destPath = path.join(UPLOAD_DIR, storedName);
32 
33 try {
34 const source = Readable.fromWeb(file.stream());
35 await pipeline(source, createWriteStream(destPath));
36 } catch {
37 return NextResponse.json({ error: 'Failed to store file' }, { status: 500 });
38 }
39 
40 return NextResponse.json(
41 { name: storedName, size: file.size, type: file.type },
42 { status: 201 },
43 );
44}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Streaming with pipeline moves bytes to disk without holding the whole file in memory.
  2. 2Validate type and size before touching the filesystem so bad requests fail cheaply.
  3. 3Generating a random stored name avoids collisions and neutralizes attacker-controlled filenames.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming file uploads in a Next.js route — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code