Next.js from Scratch: Fundamentals Applied in a Real SaaS Project
This post covers Next.js from first principles, then shows exactly how each concept is applied in Senquel. If you've seen the framework name but never built with it, this is the grounded introduction. If you already know React, it explains what Next.js adds and why those additions matter for a production SaaS app.
1. What is Next.js?
React is a UI library. It gives you components, state, and rendering — but nothing else. To build a real app you also need routing, a server, API endpoints, and a way to ship optimised HTML to the browser. Next.js provides all of that, assembled into a single framework with sensible defaults.
What Next.js adds on top of React
React alone
- Components and props
- State and hooks
- Virtual DOM rendering
- JSX syntax
Next.js adds
- File-based routing
- Server-side rendering
- API routes (built-in backend)
- Middleware (runs before routes)
- Image + font optimisation
- TypeScript out of the box
2. Creating a Next.js app from scratch
One command bootstraps a complete project:
npx create-next-app@latest my-project
# Options you'll be asked:
# ✓ TypeScript? Yes
# ✓ ESLint? Yes
# ✓ Tailwind CSS? Yes
# ✓ App Router? Yes ← always yes for new projects
# ✓ src/ directory? No (project root is cleaner)
# ✓ import alias @/*? Yes
This gives you a working dev server on localhost:3000. Run it with:
npm run dev
That's it. No Webpack config, no Babel setup, no manual routing library — the framework handles all of it.
3. The project structure
After scaffolding, your project looks like this. Senquel follows this structure exactly.
Project layout
my-project/ ├── app/ ← every page and API route lives here │ ├── layout.tsx ← root HTML shell (wraps every page) │ ├── page.tsx ← the / route │ ├── blog/ │ │ ├── page.tsx ← /blog │ │ └── [slug]/ │ │ └── page.tsx ← /blog/any-slug │ ├── features/ │ │ └── project-creation/ │ │ └── page.tsx ← /features/project-creation │ └── api/ │ └── whatsapp/ │ └── webhook/ │ └── route.ts ← POST /api/whatsapp/webhook ├── lib/ ← shared utilities, Supabase helpers, types ├── public/ ← static files served as-is (images, videos) ├── middleware.ts ← runs before every request └── next.config.ts ← framework configuration
The key rule: the folder path inside app/ is the URL. app/blog/[slug]/page.tsx becomes /blog/:slug. No separate router configuration — the file system is the router.
4. File-based routing in depth
Next.js App Router uses special filenames inside each folder. Understanding what each file does is the foundation of everything else.
Special filenames in App Router
| File | Purpose | In Senquel |
|---|---|---|
| page.tsx | Renders the UI for this route | All feature pages, blog posts |
| layout.tsx | Wraps child pages — persists across navigations | Root HTML + font loading |
| route.ts | API endpoint — exports GET, POST, etc. | WhatsApp webhook, Xero OAuth, firm invite |
| loading.tsx | Shown while the page is streaming | Not used yet |
| [slug] | Dynamic segment — value passed as a param | app/blog/[slug]/page.tsx |
5. Server vs Client components — the most important concept
Every component in Next.js App Router is a Server Component by default. Server Components run exclusively on the server — they never ship JavaScript to the browser, they can directly access databases, and they can be async functions. They cannot use hooks or browser APIs.
Add 'use client' at the top of a file to make it a Client Component. Client Components are sent to the browser as JavaScript — they support useState, useEffect, event listeners, and all of React's interactive APIs.
Server vs Client components
In Senquel: blog posts are Server Components (no JS cost). Feature pages are Client Components (interactive forms, state).
The practical rule: default to Server Components. Only add 'use client' when you need interactivity, hooks, or browser APIs. In Senquel, every page under app/features/ starts with 'use client' because they manage form state, show loading indicators, and respond to user events.
6. API Routes — the built-in backend
Any file named route.ts inside app/api/ becomes an HTTP endpoint. You export functions named after the HTTP methods you want to handle:
// app/api/whatsapp/webhook/route.ts
export async function GET(request: Request) {
// Handles GET /api/whatsapp/webhook
// Used by Meta to verify the webhook on setup
}
export async function POST(request: Request) {
// Handles POST /api/whatsapp/webhook
// Called by Meta/Twilio when a WhatsApp message arrives
}
These run on the server only — no browser JavaScript is involved. They can access secrets, call external APIs, write to the database. In Senquel, /api/whatsapp/webhook receives incoming WhatsApp messages, looks up the project by phone number, parses the command, and calls the appropriate Supabase helper.
How a WhatsApp message becomes a database write
7. Middleware — the request interceptor
The file middleware.ts at the project root runs before every matched request. It receives the request and returns either the original response (passing through), a redirect, or a rewrite. It runs at the edge — before any page component or API route handler executes.
// middleware.ts
export async function middleware(request: NextRequest) {
// Redirect senquel.com/about → /demo/product-2
const host = request.headers.get('host') ?? '';
if (host.includes('senquel.com') && request.nextUrl.pathname === '/about') {
return NextResponse.redirect(new URL('/demo/product-2', request.url));
}
// Auth guard — /features/* requires a signed-in user
const { data: { user } } = await supabase.auth.getUser();
if (path.startsWith('/features') && !user) {
return NextResponse.redirect(new URL('/launch', request.url));
}
return supabaseResponse; // pass through
}
Request lifecycle with middleware
The matcher config controls which paths trigger middleware. In Senquel, API routes are excluded so webhook handlers never get intercepted by the auth logic:
export const config = {
matcher: [
// Skip: static files, images, API routes
'/((?!_next/static|_next/image|favicon.ico|api/|.*\.(?:svg|png|mp4)$).*)',
],
};
8. Data fetching — three patterns in Senquel
Next.js supports multiple data fetching strategies. Senquel uses three, each for a different context:
The three Supabase clients in Senquel
The critical security rule: the service client uses SUPABASE_SERVICE_ROLE_KEY — a secret that bypasses all Row Level Security. It must never appear in client-side code or be prefixed with NEXT_PUBLIC_. Senquel uses it only inside API route handlers where no browser can reach it.
9. Environment variables — public vs secret
Next.js has a simple but strict rule for environment variables:
| Prefix | Available in | Senquel example |
|---|---|---|
| NEXT_PUBLIC_ | Browser + server | NEXT_PUBLIC_SUPABASE_URL |
| (no prefix) | Server only — never reaches browser | SUPABASE_SERVICE_ROLE_KEY TWILIO_AUTH_TOKEN ANTHROPIC_API_KEY |
If you accidentally add NEXT_PUBLIC_ to a secret key, Next.js embeds it in the JavaScript bundle shipped to every visitor. This is a real security incident — check your prefixes carefully.
10. TypeScript and path aliases
Next.js ships with TypeScript support configured automatically. The @/ path alias (set in tsconfig.json) lets you import from the project root without long relative paths:
// Without alias — fragile, breaks when you move files
import { createProject } from '../../../lib/supabase/projects';
// With @/ alias — always resolves from project root
import { createProject } from '@/lib/supabase/projects';
Every file in Senquel uses the @/ alias. It's defined once in tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}
11. The full Senquel request flow
Putting it all together: here's what happens when a designer opens the Milestone Tracker page.
GET /features/milestone-tracker — full flow
What to build next
If you're starting a new Next.js project from scratch, here's the sequence that works:
- Scaffold with
create-next-app— TypeScript, App Router, Tailwind, yes to all. - Add your database — Supabase gives you Postgres, auth, and real-time out of the box with Next.js SSR support via
@supabase/ssr. - Wire middleware — protect your auth-required routes immediately, before building features. It's two dozen lines and saves hours of debugging later.
- Build features as Client Components —
'use client'pages withuseEffectfor data fetching is the fastest path to a working UI. Optimise to Server Components later where it matters for performance. - Add API routes for external integrations — webhooks, OAuth callbacks, third-party APIs. Keep secrets out of client code from day one.
The Senquel codebase follows this sequence exactly — the migrations folder tells the story of what was added in what order. If you want to read it from the beginning, start at supabase/migrations/001_multi_tenant.sql and work forward.