When you build a normal React app (npm run build), you get a dist/ folder: static HTML, JS, and CSS files that any web server or CDN can serve. There's no server involved.
Next.js is different. Run npm run build and you get a .next/ folder that contains:
This means you can't just upload a Next.js build to a static host like GitHub Pages. You need a host that understands Next.js - Vercel being the most common choice (Vercel is the company that builds Next.js).
# React(Vite) build output
dist/
├── index.html
├── assets/
│ ├── index-abc123.js
│ └── index-abc123.css
# Next.js build output (.next/)
.next/
├── static/ ← static assets, CSS, JS chunks
├── server/ ← server-rendered pages and routes
├── cache/ ← build cache
└── BUILD_ID ← unique ID for this build
You can export a fully static Next.js site with output: 'export' in next.config.ts if your app has no server-side features. But as soon as you use Server Functions, server-side rendering, or API routes, you need a proper server.
<aside> ⌨️
Hands on: Run npm run build in your project. Read the output carefully - find the table showing static (○) and dynamic (λ) routes. If a route you expected to be static is showing as dynamic, check whether it uses cache: 'no-store' or cookies()/headers() - those force dynamic rendering. Also find the "First Load JS" column: pages with a large bundle are shipping a lot of JavaScript to the browser.
</aside>
Next.js distinguishes between server-side and client-side environment variables. This matters for security: you don't want API keys ending up in the JavaScript bundle that ships to every browser.
These are available only during server-side rendering and in Server Functions. They're never included in the browser bundle.
# .env.local
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=xx_...
API_KEY=xx...
// app/checkout/actions.ts - runs on the server, safe
"use server"
export async function createPaymentIntent() {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) // ✅ server-only
}
NEXT_PUBLIC_Prefix a variable with NEXT_PUBLIC_ to include it in the browser bundle. Only do this for values that are safe to expose publicly (analytics IDs, public API URLs, feature flags).
# .env.local
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXXXXX
NEXT_PUBLIC_API_URL=https://api.example.com
// components/Analytics.tsx - runs in the browser
"use client"
console.log(process.env.NEXT_PUBLIC_ANALYTICS_ID) // ✅ available
console.log(process.env.DATABASE_URL) // ❌ undefined - stripped from bundle
<aside> ⚠️
If you put a secret key in NEXT_PUBLIC_, it will be visible in the JavaScript source that any user can inspect. Never prefix secrets with NEXT_PUBLIC_.
</aside>
.env file precedenceNext.js loads environment files in this order (later overrides earlier):
.env - defaults committed to source control.env.local - local overrides, never commit this file (add to .gitignore).env.production / .env.development - environment-specific<aside> ⌨️
Hands on: Create a .env.local file in your project with two variables: MY_SECRET=server-only and NEXT_PUBLIC_APP_NAME=My Portfolio. Add console.log(process.env.MY_SECRET) to a Server Component and console.log(process.env.NEXT_PUBLIC_APP_NAME) to a Client Component ("use client"). Run the dev server and check where each log appears. Then look at the built JavaScript bundle - open DevTools, search for the variable values to confirm MY_SECRET is not there but NEXT_PUBLIC_APP_NAME is. Don’t forget to add .env.local to .gitignore.
</aside>
That's it. Vercel runs npm run build, deploys the output, and gives you a URL.
In the Vercel dashboard:
<aside> ⚠️
Adding an environment variable in the Vercel dashboard does not automatically apply it to your current deployment. You must trigger a new deployment (redeploy) for the change to take effect.
</aside>
After a deploy, Vercel shows you the build output - which routes are static (⚡ fast) and which are dynamic (λ server-rendered). This is useful for spotting accidental dynamic routes.
Route (app) Size First Load JS
┌ ○ / 1.2 kB 87.5 kB
├ ○ /about 839 B 87.2 kB
├ λ /products 1.5 kB 88.1 kB
└ λ /products/[id] 2.1 kB 88.5 kB
○ (Static) prerendered as static content
λ (Dynamic) server-rendered on demand
NEXT_PUBLIC_ variables are baked into the JavaScript bundle at build time. If you change one in Vercel, you must redeploy - just saving the new value isn't enough, because the old bundle still has the old value hardcoded.To redeploy without a code change: Deployments → find the latest deployment → Redeploy.
Always test a production build locally before deploying:
npm run build # build the .next/ folder
npm run start # serve it on localhost:3000
This catches issues that only appear in production mode (missing env vars, server-only code accidentally called on the client, etc.).
The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.