Introduction
Building a serious video player inside a Next.js app sounds straightforward until you start listing the work. Uploads, thumbnails (often auto-generated), watermarks, playback controls, quality switching, format conversion, secure server-side auth for uploads, a database to track metadata, and routes for watching individual videos. That’s a stack, not a feature.
The shortcut most production teams take is to push the heavy lifting — storage, transformation, optimisation, delivery — to an image and video API. This guide walks through building a complete video upload-and-playback app in Next.js using ImageKit as the media layer. It covers the upload pipeline, server-side auth handshake, URL-based transformations (thumbnails, watermarks, quality, format), and the player itself. Output is a clean base you can extend for any video-driven product.
📚 Table of contents
- The architecture: client uploads, server-issued tokens
- Project scaffold and environment variables
- The upload-auth route — the security boundary
- Storing video metadata (local JSON, swap for a real DB later)
- The upload UI and end-to-end flow
- The video library: auto-generated thumbnails from a URL
- The simple player: transformations, posters, quality control
- Watermarks via URL transformations
- Adaptive bitrate streaming for production
- Common mistakes & pro tips
- Frequently asked questions
🧱 The architecture
The single design decision that drives the whole app: never put your ImageKit private key in the browser. The client asks the Next.js backend for a short-lived signed token, then uses that token to upload directly to ImageKit. The backend stays in control of who can upload, how often, and how large.
- Client calls
/api/upload-auth. - Server returns a token, signature, and expiration using the ImageKit private key.
- Client uploads directly to ImageKit using the token.
- Client posts the resulting file path and metadata to
/api/videos. - Server persists metadata; client redirects to
/watch/[id].
🛠️ Project scaffold
Create a Next.js app with the App Router and TypeScript. Install the ImageKit Next.js SDK and the
uuid package for unique video ids. Add three env vars:
NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT— safe to expose to the browser.IMAGEKIT_PUBLIC_KEY— safe to expose to the browser.IMAGEKIT_PRIVATE_KEY— server-side only. Used to sign upload tokens.
Folder layout: src/app/api/upload-auth/route.ts, src/app/api/videos/route.ts
(list + create), src/app/api/videos/[id]/route.ts (single video), and
src/app/watch/[id]/page.tsx for the watch page. Components live under
src/components/video/ with a nested player/ folder.
🔐 The upload-auth route
This is the security boundary. The route reads both keys from the environment, calls
getUploadAuthParameters from @imagekit/next/server, and returns a JSON object
with a token, signature, expire timestamp, and the public key. The client never sees the private key.
import { getUploadAuthParameters } from "@imagekit/next/server";
import { NextResponse } from "next/server";
export async function GET() {
try {
const privateKey = process.env.IMAGEKIT_PRIVATE_KEY!;
const publicKey = process.env.IMAGEKIT_PUBLIC_KEY!;
const auth = getUploadAuthParameters({ privateKey, publicKey });
return NextResponse.json({ ...auth, publicKey });
} catch (err) {
return NextResponse.json(
{ error: "Failed to generate upload credentials" },
{ status: 500 }
);
}
}
Why short-lived tokens matter
Each token authorises one upload and expires quickly. Even if it leaks, the blast radius is a single file. Long-lived API keys are a much larger risk.
Wrap the handler in try/catch, return 500 with a generic error message if
something fails, and don’t leak the underlying exception to the client.
📁 Storing video metadata
To keep the tutorial focused, store metadata in a local data/videos.json file. The
TypeScript record:
export interface WatermarkConfig {
imagePath: string;
position: "top_left" | "top_right" | "bottom_left" | "bottom_right";
opacity?: number;
width?: number;
}
export interface Video {
id: string;
title: string;
description: string;
filePath: string;
fileName: string;
thumbnailPath?: string;
duration?: number;
createdAt: string;
watermark?: WatermarkConfig;
quality?: number;
format?: "auto" | "mp4" | "webm";
}
Expose getAllVideos, getVideoById, and saveVideo from
src/lib/video-storage.ts. In production swap this for Postgres, Supabase, or any database
of choice — the function signatures stay the same.
🎬 The upload UI
The VideoUpload component is a client component holding form state for title,
description, video file, optional thumbnail, and optional watermark. On submit it:
- Calls
/api/upload-authfor the token. - Uses
upload()from@imagekit/nextto push the video, thumbnail, and watermark in parallel viaPromise.all. - Posts the resulting paths plus user-entered metadata to
/api/videos. - Redirects to
/watch/[id]on success.
Free-tier ImageKit caps uploads at around 20–40 MB. Surface that error in the UI so users know why a large file failed.
📚 The video library — auto-generated thumbnails
The home page lists all videos with thumbnails. The clever ImageKit trick: if you don’t upload a
thumbnail, append /ik-thumbnail.jpg to the video URL and ImageKit returns a thumbnail
image generated from the first frame — cached after the first request.
👉
Pair this with the <Image> component from @imagekit/next and pass a
transformation array ([{ width: 320, height: 180 }]) to deliver
optimally-sized thumbnails for every breakpoint.
🎛️ The simple player — transformations, posters, quality
The SimplePlayer component takes a video record and the URL endpoint. It builds a
transformation array that handles quality, optional format conversion, and an optional watermark
overlay. The watermark uses the image-overlay transformation with type: 'image',
input (the watermark path), position, and transformation: [{ width: 120 }].
Quality switching
- Local
qualitystate (1–100) drives a number input. - The
<Video>component keys offqualityso it remounts on change. - Anything below ~80 still looks great and dramatically reduces bandwidth.
buildSrcgenerates the poster URL with its own transformation (1920x1080).
Add suppressHydrationWarning on the quality input to silence the React hydration
warning that pops up on browser-only state.
🖼️ Watermarks via URL transformations
ImageKit transformations are query-parameter overlays on the asset URL. To watermark a video, upload a watermark image once, then pass it as an image overlay in the player’s transformation array. The watermark appears in the chosen corner with the chosen opacity and width.
Because the transformation lives in the URL, the watermarked video plays anywhere — you could paste the URL into a browser tab and the watermark would still be baked in. That makes it trivial to test in isolation.
⚡ Adaptive bitrate streaming for production
Once you ship past a few users, the static-MP4-with-quality-slider approach hits a ceiling. ImageKit
exposes adaptive bitrate streaming via the ik-master transformation, returning an HLS
manifest that automatically scales quality to the viewer’s bandwidth.
The trade-off: not every URL transformation (overlays, format conversion) composes cleanly with HLS. For most apps the right pattern is two delivery modes — rich-transformation MP4 for marketing or hero pieces, adaptive HLS for the long tail.
💡 Common mistakes & pro tips
❌ Common mistakes
- Leaking
IMAGEKIT_PRIVATE_KEYto the browser by prefixing itNEXT_PUBLIC_. - Using the wrong env var name on the server (
IMAGEKIT_URL_ENDPOINTvsNEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT). - Hard-coding transformation widths instead of matching CSS breakpoints.
- Skipping the
try/catchon the upload-auth route and leaking stack traces.
✅ Pro tips
- Generate thumbnails via
/ik-thumbnail.jpgfirst; only upload custom ones for hero videos. - Cap upload size on the backend before issuing a token.
- Cache poster URLs — transformation generation is fast but not free on first hit.
- Use a real database from day one if you expect > 100 videos.
Conclusion
A real video feature is a stack of small problems — auth, uploads, thumbnails, watermarks, playback, optimisation, delivery. Pushing the media-handling pieces to a URL-driven API and keeping your Next.js app focused on the auth boundary, metadata, and player UI cuts the work to a single weekend. From here, the obvious next steps are a real database, signed playback URLs for private content, captions, and HLS for adaptive delivery once your audience grows.
Related reading: OnSpace AI mobile app builder tutorial — Claude Code hands-on guide — Cursor AI review — Postgres TimescaleDB time-series fix — learn Python in 2026: zero to specialization