javascript 50 lines · 6 steps

Serving private assets from a Next.js route

A Next.js route handler streams files from a private directory with path-traversal guards and ETag caching.

Explained by highlit
1import { NextResponse } from 'next/server';
2import { readFile, stat } from 'node:fs/promises';
3import { join, extname, normalize } from 'node:path';
4import { createHash } from 'node:crypto';
5 
6const ASSETS_DIR = join(process.cwd(), 'private', 'assets');
7 
8const MIME_TYPES = {
9 '.css': 'text/css; charset=utf-8',
10 '.js': 'text/javascript; charset=utf-8',
11 '.svg': 'image/svg+xml',
12 '.png': 'image/png',
13 '.jpg': 'image/jpeg',
14 '.woff2': 'font/woff2',
15};
16 
17export async function GET(request, { params }) {
18 const { slug } = await params;
19 const relative = normalize(slug.join('/'));
20 
21 if (relative.startsWith('..')) {
22 return new NextResponse('Not found', { status: 404 });
23 }
24 
25 const filePath = join(ASSETS_DIR, relative);
26 
27 let contents, info;
28 try {
29 [contents, info] = await Promise.all([readFile(filePath), stat(filePath)]);
30 } catch {
31 return new NextResponse('Not found', { status: 404 });
32 }
33 
34 const etag = `"${createHash('sha1').update(contents).digest('hex')}"`;
35 
36 if (request.headers.get('if-none-match') === etag) {
37 return new NextResponse(null, { status: 304, headers: { ETag: etag } });
38 }
39 
40 return new NextResponse(contents, {
41 status: 200,
42 headers: {
43 'Content-Type': MIME_TYPES[extname(filePath)] ?? 'application/octet-stream',
44 'Content-Length': String(info.size),
45 'Cache-Control': 'public, max-age=31536000, immutable',
46 'Last-Modified': info.mtime.toUTCString(),
47 ETag: etag,
48 },
49 });
50}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Normalizing and rejecting `..` paths keeps user input from escaping the intended directory.
  2. 2Content-hashed ETags let clients skip re-downloading unchanged files with a 304 response.
  3. 3Mapping extensions to MIME types with a fallback ensures browsers interpret every response correctly.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Serving private assets from a Next.js route — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code