Full Stack Web Dev
Student Handbook
A practical, ground-up guide to building and deploying modern web applications — covering everything from Git basics to production deployments on Vercel and Railway.
Dev Tools You Must Install
Essential VS Code Extensions
Git & GitHub
Git is a tool that tracks changes to your code over time. GitHub is a website that hosts your code (repositories) online. Think of Git like a save system for your project — except it saves the full history, and you can roll back to any point.
Core Git Concepts
| Term | What It Means | Analogy |
|---|---|---|
| Repository (repo) | Your project folder tracked by Git | The entire project folder |
| Commit | A saved snapshot of your code at a point in time | Hitting "Save" in a game |
| Branch | A parallel copy of your code to work on without affecting main | A draft copy of a document |
| Merge | Combining two branches back together | Accepting changes from a draft into the final doc |
| Push | Upload your local commits to GitHub | Uploading to Google Drive |
| Pull | Download latest commits from GitHub to local | Syncing from Google Drive |
| Clone | Download a GitHub repo to your machine for the first time | Downloading a project folder |
| .gitignore | File listing paths Git should NOT track | A "don't save this" list |
The Daily Git Workflow
Before anything, see what files you've modified.
Tell Git which files to include in the next commit.
Describe what you did clearly. Future-you will thank you.
Upload your commits so they're backed up and shareable.
# ── First-time setup ───────────────────────────────────
git config --global user.name "Your Name"
git config --global user.email "you@email.com"
# ── Start a new project ────────────────────────────────
git init # initialise git in current folder
git remote add origin https://github.com/you/repo.git
# ── Clone an existing repo ─────────────────────────────
git clone https://github.com/you/repo.git
# ── Daily workflow ─────────────────────────────────────
git status # see changed files
git add . # stage everything
git add src/app/page.tsx # or stage a specific file
git commit -m "feat: add login page"
git push origin main # push to GitHub
# ── Branches ───────────────────────────────────────────
git checkout -b feature/dashboard # create and switch to new branch
git checkout main # go back to main
git merge feature/dashboard # merge feature into main
# ── Undo mistakes ──────────────────────────────────────
git restore filename.tsx # discard uncommitted changes to a file
git reset --soft HEAD~1 # undo last commit, keep changes staged
Commit Message Convention
Use a prefix to describe what type of change you made. This makes your history readable at a glance.
| Prefix | When to use | Example |
|---|---|---|
feat: |
New feature or functionality | feat: add user registration |
fix: |
Bug fix | fix: login form not submitting |
chore: |
Config, deps, setup — no logic change | chore: install tailwindcss |
style: |
UI/CSS changes only | style: update hero section padding |
refactor: |
Code cleanup without changing behavior | refactor: extract fetchUser helper |
docs: |
README or documentation changes | docs: update setup instructions |
What Goes in .gitignore
# Dependencies
node_modules/
# Environment secrets
.env
.env.local
.env.production
# Build outputs
.next/
dist/
build/
# OS files
.DS_Store
Thumbs.db
# Editor files
.vscode/settings.json
.env
files. They contain secret keys. If you accidentally push secrets to GitHub, rotate them immediately — bots
scan GitHub for exposed keys within minutes.
HTML / CSS / JavaScript
Every website ultimately runs HTML, CSS, and JavaScript in the browser. Frameworks like React and Next.js are just tools that help you write and organise these three things more efficiently. You don't need to master vanilla JS, but you need to understand the concepts.
HTML — Structure
HTML defines what is on the page — headings, paragraphs, buttons, images, links.
<!-- HTML = content structure -->
<div class="card">
<h2>Patient Dashboard</h2>
<p>Heart rate: <span id="bpm">72</span> BPM</p>
<button onclick="refresh()">Refresh</button>
</div>
CSS — Styling
CSS defines how things look — colours, fonts, layout, spacing, animations. In a Next.js project with Tailwind you write CSS as utility classes rather than separate CSS files.
/* Traditional CSS */
.card {
background: #ffffff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
/* Tailwind equivalent (in JSX className) */
// className="bg-white rounded-xl p-6 shadow-lg"
JavaScript — Behaviour
JS makes the page interactive and handles data. Learn these fundamentals before React:
| Concept | Why it matters |
|---|---|
Variables
—
const
,
let
|
Store and update data |
| Functions & arrow functions | Reusable blocks of logic — used constantly in React |
| Arrays & objects | How JSON data (from APIs) is structured |
Array methods
—
.map()
,
.filter()
,
.find()
|
Used in React to render lists, filter data |
| Async / await & fetch() | Calling your backend API from the frontend |
| Destructuring | Unpacking object properties — very common in React |
| Spread operator (...) | Merging/copying objects and arrays |
| Template literals |
Building strings with variables:
`Hello ${name}`
|
React
React is a JavaScript library made by Meta for building UIs out of reusable components . Instead of directly manipulating the HTML, you describe what the UI should look like, and React updates the DOM efficiently.
Core Concepts You Must Know
1. Components
Everything in React is a component — a JavaScript function that returns JSX (HTML-like syntax).
// A simple React component
function GaugeCard({ label, value, unit, status }) {
return (
<div className=`card ${status}`>
<span className="label">{label}</span>
<span className="value">{value} {unit}</span>
</div>
);
}
// Using it — pass data as props
<GaugeCard label="Heart Rate" value=72 unit="BPM" status="normal" />
2. Props
Props (properties) are how you pass data into a component. They flow one-way: parent → child. You cannot modify props inside the component.
3. State — useState
State is data that lives inside a component and can change. When state changes, React re-renders the component automatically.
import { useState } from "react";
function Counter() {
// [currentValue, functionToUpdateIt] = useState(initialValue)
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
4. useEffect — Side Effects
Used for anything that happens outside React's render cycle: fetching data, setting up timers, subscribing to events.
import { useState, useEffect } from "react";
function LiveDashboard() {
const [reading, setReading] = useState(null);
useEffect(() => {
// This runs after the component renders
const fetchData = async () => {
const res = await fetch("/api/readings/latest");
const data = await res.json();
setReading(data);
};
fetchData(); // fetch immediately
const interval = setInterval(fetchData, 5000); // then every 5s
return () => clearInterval(interval); // cleanup on unmount
}, []); // [] = run once on mount
if (!reading) return <p>Loading...</p>;
return <p>BPM: {reading.bpm}</p>;
}
5. Conditional Rendering & Lists
// Conditional rendering
{isLoading ? <Spinner /> : <Dashboard />}
{error && <ErrorBanner message={error} />}
// Rendering a list
{alerts.map((alert) => (
<AlertRow key={alert.id} data={alert} />
))}
key
prop when rendering lists. It helps React efficiently update items. Use a unique ID from your data, not the
array index.
Next.js
Next.js is a framework built on top of React that adds: file-based routing, server-side rendering, API routes, image optimisation, and more. It's what most modern web apps use for FYPs because it handles both frontend and lightweight backend in one codebase.
Folder Structure (App Router)
my-app/
├── app/
│ ├── layout.tsx # Root layout — wraps ALL pages
│ ├── page.tsx # Home page → /
│ ├── about/
│ │ └── page.tsx # About page → /about
│ ├── dashboard/
│ │ ├── page.tsx # Dashboard → /dashboard
│ │ └── loading.tsx # Auto loading state
│ └── api/
│ └── readings/
│ └── route.ts # API endpoint → /api/readings
├── components/ # Reusable UI components
├── lib/ # Utilities, db clients, helpers
├── public/ # Static files (images, fonts)
├── .env.local # Secret environment variables
└── next.config.ts
File Routing Rules
| File Path | URL | Notes |
|---|---|---|
app/page.tsx |
/ |
Home page |
app/about/page.tsx |
/about |
Static route |
app/blog/[slug]/page.tsx |
/blog/my-post |
Dynamic route —
slug
is a param
|
app/api/users/route.ts |
/api/users |
API endpoint |
app/layout.tsx |
Wraps all pages | Add nav, footer, providers here |
app/loading.tsx |
Shown while page loads | Automatic — just create the file |
app/not-found.tsx |
404 page | Shown for unmatched routes |
Server vs Client Components
This is the most important Next.js concept to understand. By default, every component is a
Server Component
— it runs only on the server. To use React hooks (
useState
,
useEffect
) you need a
Client Component
.
| Feature | Server Component | Client Component |
|---|---|---|
| Runs on | Server only | Browser (after hydration) |
| Can use useState / useEffect | ❌ No | ✅ Yes |
| Can directly query DB | ✅ Yes | ❌ No |
| SEO-friendly | ✅ Yes | Partial |
| How to make it | Default (do nothing) |
Add
"use client"
at top of file
|
// ── Server Component (default) ───────────────────────
// Can fetch DB data directly. No "use client" needed.
async function ProductsPage() {
const products = await db.query("SELECT * FROM products");
return <ProductList items={products} />;
}
// ── Client Component ─────────────────────────────────
"use client"; // This directive MUST be the first line
import { useState } from "react";
export function SearchBar() {
const [query, setQuery] = useState("");
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
API Routes
Next.js can serve backend API endpoints without a separate server. These live in
app/api/*/route.ts
.
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
// GET /api/users
export async function GET() {
const users = await fetchUsersFromDB();
return NextResponse.json(users);
}
// POST /api/users
export async function POST(req: NextRequest) {
const body = await req.json();
const newUser = await createUser(body);
return NextResponse.json(newUser, { status: 201 });
}
Getting Started
# Create a new Next.js project
npx create-next-app@latest my-fyp-app
# Options to select during setup:
# ✅ TypeScript ✅ Tailwind CSS ✅ App Router ✅ ESLint
cd my-fyp-app
npm run dev # starts dev server at http://localhost:3000
Tailwind CSS
Tailwind is a CSS framework where instead of writing
.card { padding: 24px; }
in a separate file, you apply pre-built utility classes directly in your HTML/JSX. It's faster for rapid
development and pairs perfectly with Next.js.
Core Utility Classes to Know
| Category | Examples | What They Do |
|---|---|---|
| Spacing |
p-4
px-6
mt-2
gap-4
|
Padding, margin, gap (multiples of 4px) |
| Layout |
flex
grid
items-center
justify-between
|
Flexbox and grid layout |
| Sizing |
w-full
h-screen
max-w-xl
|
Width, height, max-width |
| Typography |
text-xl
font-bold
text-gray-500
|
Font size, weight, colour |
| Colour |
bg-white
text-blue-600
border-gray-200
|
Background, text, border colours |
| Borders |
border
rounded-xl
shadow-md
|
Border, border-radius, shadows |
| Responsive |
md:flex
lg:grid-cols-3
|
Apply class only at certain breakpoints |
| Hover/Focus |
hover:bg-blue-700
focus:outline-none
|
State-based styling |
// A styled card component with Tailwind
function ReadingCard({ label, value, color }) {
return (
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
<p className="text-sm text-gray-500 uppercase tracking-wide font-medium">
{label}
</p>
<p className=`text-4xl font-bold mt-2 ${color}`>
{value}
</p>
</div>
);
}
Backend Concepts
The backend is the part of your app users never see. It's a server that receives requests, runs business logic, talks to a database, and sends responses. Your FYP may have a separate backend (Python/FastAPI, Node/Express) or use Next.js API routes — both are valid.
Backend Frameworks by Language
Key Backend Concepts
Authentication vs Authorisation
| Term | Meaning | Example |
|---|---|---|
| Authentication | "Who are you?" — verifying identity | Login with email + password |
| Authorisation | "What can you do?" — permission check | Only admins can delete records |
| JWT | JSON Web Token — a signed token proving who you are | Stored in cookie/localStorage after login |
| Session | Server-side record of who is logged in | Alternative to JWTs |
Middleware
Functions that run between the request arriving and your route handler running. Common uses: checking auth headers, logging, rate limiting, CORS.
CORS (Cross-Origin Resource Sharing)
A browser security rule. If your frontend is on
myapp.vercel.app
and your backend is on
api.railway.app
, the browser blocks requests by default. You must configure your backend to allow your frontend's origin.
# FastAPI CORS example
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.vercel.app"], # your Vercel URL
allow_methods=["*"],
allow_headers=["*"],
)
REST API Design
REST (Representational State Transfer) is a set of conventions for designing HTTP APIs. Your frontend calls these URLs to get or change data. The rules are simple but important.
HTTP Methods
| Method | Action | Example |
|---|---|---|
| GET | Read / fetch data |
GET /api/users
— get all users
|
| POST | Create new data |
POST /api/users
— create a user
|
| PUT | Replace / update (full) |
PUT /api/users/5
— replace user 5
|
| PATCH | Partial update |
PATCH /api/users/5
— update one field
|
| DELETE | Delete data |
DELETE /api/users/5
— delete user 5
|
HTTP Status Codes You Must Know
| Code | Name | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE |
| 201 | Created | Successful POST that created a record |
| 400 | Bad Request | Invalid input or malformed request body |
| 401 | Unauthorized | No valid auth token provided |
| 403 | Forbidden | Authenticated but not allowed to do this |
| 404 | Not Found | Resource doesn't exist |
| 422 | Unprocessable Entity | Validation failed (FastAPI uses this) |
| 500 | Internal Server Error | Something crashed on the server |
Calling an API from Next.js (frontend)
// Using fetch() to call your backend
async function getReadings() {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/readings/latest`);
if (!res.ok) {
throw new Error("Failed to fetch readings");
}
return res.json();
}
// POST example
async function createReading(data) {
const res = await fetch("/api/readings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.json();
}
fetch
in
useEffect
. They handle loading/error states, caching, and refetching automatically.
SQL & PostgreSQL
A database stores your app's data permanently. PostgreSQL (often called Postgres) is the most popular open-source relational database and the one Supabase uses under the hood.
Core SQL Commands
-- Create a table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
name TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Insert a row
INSERT INTO users (email, name) VALUES ('ali@example.com', 'Ali');
-- Query rows
SELECT * FROM users;
SELECT email, name FROM users WHERE name = 'Ali';
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
-- Update
UPDATE users SET name = 'Ahmad' WHERE id = 'some-uuid';
-- Delete
DELETE FROM users WHERE id = 'some-uuid';
-- JOIN — combine data from two tables
SELECT orders.id, users.name, orders.total
FROM orders
INNER JOIN users ON orders.user_id = users.id;
ORMs — Avoiding Raw SQL
An ORM (Object-Relational Mapper) lets you interact with your database using your language's syntax instead of SQL strings. Strongly recommended for FYPs.
| ORM | Language | Notes |
|---|---|---|
| Prisma | TypeScript/JS | Best TypeScript ORM. Auto-generates types. Use with Next.js. |
| Drizzle | TypeScript/JS | Lighter than Prisma, more SQL-like syntax. Good for edge deployments. |
| SQLAlchemy | Python | The standard Python ORM. Used with FastAPI/Django backends. |
// Prisma example — query users in TypeScript
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// Same as: SELECT * FROM users ORDER BY created_at DESC LIMIT 10
const users = await prisma.user.findMany({
orderBy: { created_at: "desc" },
take: 10,
});
Important Concepts
| Concept | Explanation |
|---|---|
| Primary Key |
A unique identifier for each row (usually
id
). Use UUID, not integer, for distributed systems.
|
| Foreign Key | A column that references the primary key of another table. Enforces relationships. |
| Index | Makes queries on a column much faster. Always index columns you filter or sort by. |
| Migration | A script that changes your DB schema (adding columns, tables). Track these in version control. |
| Transactions | Group multiple DB operations so they all succeed or all fail together (atomic). |
Supabase
Supabase gives you a hosted PostgreSQL database with a web dashboard, automatic REST API, authentication, file storage, and realtime subscriptions — all without managing your own server. It's the fastest way to add a database to a Next.js FYP.
What Supabase Gives You
Setup Steps
Sign up → New Project → Choose Singapore region (closest to Malaysia). Note the database password — you can't recover it.
Go to SQL Editor → paste your CREATE TABLE SQL → run it. Or use the Table Editor GUI.
Settings → Database. You need:
SUPABASE_URL
,
SUPABASE_ANON_KEY
, and the
DATABASE_URL
for direct Postgres access.
In your Next.js project:
npm install @supabase/supabase-js
. Then initialise the client with your env variables.
// lib/supabase.ts — initialise the client
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Query data from a component
const { data, error } = await supabase
.from("readings")
.select("*")
.order("recorded_at", { ascending: false })
.limit(10);
// Insert data
const { data, error } = await supabase
.from("readings")
.insert({ spo2: 97.5, bpm: 72, temperature: 36.6 });
// Realtime subscription
supabase
.channel("readings")
.on("postgres_changes", { event: "INSERT", schema: "public", table: "readings" },
(payload) => console.log("New reading:", payload.new))
.subscribe();
Supabase Free Tier Limits (as of 2025)
| Resource | Free Tier | Notes |
|---|---|---|
| Database size | 500 MB | Enough for most FYPs |
| Auth users | 50,000 | More than enough |
| Storage | 1 GB | For file uploads |
| Projects | 2 active | Pause inactive ones to have more |
| Pausing | After 1 week inactive | Send a query weekly to keep it alive |
Vercel
Vercel is the company that made Next.js, and their platform is specifically optimised for it. Connecting your
GitHub repo to Vercel gives you automatic deployments — every push to
main
deploys your app automatically.
How Vercel Works
Sign up at vercel.com with GitHub → New Project → Import your repo. Vercel auto-detects Next.js.
In Vercel Project Settings → Environment Variables. Add
NEXT_PUBLIC_SUPABASE_URL
,
NEXT_PUBLIC_SUPABASE_ANON_KEY
, and any other secrets.
Every
git push
to your
main
branch triggers a new deployment. Feature branches get preview URLs.
Project Settings → Domains → Add your domain from Hostinger. Vercel gives you instructions to update DNS.
Key Features
| Feature | What It Does |
|---|---|
| Preview Deployments | Every pull request / branch gets its own unique URL. Share with supervisors for review. |
| Edge Network (CDN) | Your static assets are served from the closest server to the user globally. |
| Analytics | Built-in Web Vitals monitoring (page load speed, etc.) |
| Environment Variables | Separate variables for Production, Preview, and Development environments. |
| Logs | See server logs (from API routes) in real-time in the Vercel dashboard. |
Common Gotchas
.env.local
variables are added in Vercel's project settings.
npm run build
before pushing.
Railway
Railway hosts your backend servers. Unlike Vercel (which is optimised for static-first Next.js frontends), Railway runs persistent servers — which you need for Python FastAPI backends, background jobs, or any long-running process.
When to Use Railway vs Vercel
| Use Vercel for | Use Railway for |
|---|---|
| Next.js frontend | Python FastAPI / Django backend |
| Next.js API routes (serverless) | Express.js server |
| Static sites | Background workers, cron jobs |
| Edge functions | Anything that needs persistent state in memory |
Deploying a FastAPI backend to Railway
This tells Railway how to build and start your app.
railway.app → New Project → Deploy from GitHub. Select repo and subfolder if it's a monorepo.
Variables tab in Railway dashboard → add DATABASE_URL, SECRET_KEY, etc.
Copy this URL and paste it as NEXT_PUBLIC_API_URL in Vercel's settings.
# railway.toml — place in your backend/ folder
[build]
builder = "NIXPACKS"
[deploy]
startCommand = "uvicorn main:app --host 0.0.0.0 --port $PORT"
healthcheckPath = "/health"
restartPolicyType = "ON_FAILURE"
# requirements.txt — Python dependencies
fastapi
uvicorn[standard]
sqlalchemy
psycopg2-binary
python-dotenv
pydantic
Railway Pricing Note
Railway moved away from a free tier. The Hobby plan is ~$5/month (≈RM23) but gives you $5 in credits, so light usage may be effectively free. For FYPs where usage is low, you typically don't pay. Monitor your usage in the Railway dashboard.
Domain & Hostinger
A custom domain like
myproject.com
makes your FYP look professional. You buy the domain from a registrar (Hostinger is popular in Malaysia for
affordable pricing), then point it at Vercel.
How Domains Work
When someone types
myproject.com
, their browser asks a DNS server "where is this?" DNS records map domain names to IP addresses or other
servers. You control these records in Hostinger's dashboard.
| DNS Record Type | What It Does | Example |
|---|---|---|
| A Record | Points domain to an IP address |
@ → 76.76.21.21
(Vercel's IP)
|
| CNAME Record | Points domain to another domain name | www → cname.vercel-dns.com |
| MX Record | Points to email server | For setting up email |
| TXT Record | Stores verification text | Used to prove you own the domain |
Step-by-Step: Hostinger → Vercel
hpanel.hostinger.com → Domains → search for your name.
.com
is ~RM30–50/year.
.my
is also an option.
Vercel Project → Settings → Domains → Add Domain → type your domain. Vercel shows you the DNS records to set.
Hostinger hPanel → Domains → DNS Zone → edit. Add the A record and CNAME record Vercel gives you.
DNS changes take 1–48 hours to propagate worldwide (usually under 30 minutes). Vercel automatically handles SSL certificates (HTTPS).
Full Stack Architecture
Option A — Next.js Only (Recommended for most FYPs)
If your backend logic is simple (CRUD operations, basic auth), keep everything in one Next.js app. Use API routes for server logic and Supabase for the database.
┌─────────────────────────────────────────────────────────┐
│ USER │
│ browser │
└──────────────────────┬──────────────────────────────────┘
│ HTTPS
┌──────────────────────▼──────────────────────────────────┐
│ VERCEL (myproject.com) │
│ │
│ Next.js App │
│ ├── /app/page.tsx → UI pages │
│ ├── /app/api/users/route.ts → Server API endpoints │
│ └── /lib/supabase.ts → DB client │
└──────────────────────┬──────────────────────────────────┘
│ DB Connection
┌──────────────────────▼──────────────────────────────────┐
│ SUPABASE │
│ PostgreSQL database │
│ Auth, Storage, Realtime │
└─────────────────────────────────────────────────────────┘
Option B — Separate Frontend + Backend (For ML/IoT FYPs)
If your backend uses Python (for ML models, IoT, data processing), you need a separate backend server on Railway.
┌──────────────────────────────────────────────────────────┐
│ USER / IoT Device │
└────────┬─────────────────────────────────────────────────┘
│ │
Browser ESP32/Device
│ │
┌────────▼──────────┐ ┌─────────────▼────────────────┐
│ VERCEL │ │ RAILWAY │
│ Next.js │────▶│ FastAPI (Python) │
│ Frontend │ │ ML Model, Business Logic │
└───────────────────┘ └─────────────┬────────────────┘
│
┌─────────────▼────────────────┐
│ SUPABASE │
│ PostgreSQL │
└──────────────────────────────┘
Typical Data Flow
Browser loads your Next.js frontend from Vercel's CDN. The HTML renders in milliseconds.
React components call your API (either Next.js API routes or Railway backend) to load dynamic content.
The server runs SQL queries against Supabase's PostgreSQL and returns JSON.
React receives the JSON, updates component state, re-renders the UI.
Common Libraries You'll Need
| Library | Purpose | Install |
|---|---|---|
| @supabase/supabase-js | Connect to Supabase from Next.js | npm i @supabase/supabase-js |
| @supabase/auth-helpers-nextjs | Supabase auth with Next.js App Router | npm i @supabase/ssr |
| prisma | ORM for PostgreSQL | npm i prisma @prisma/client |
| zod | Schema validation for forms and API inputs | npm i zod |
| recharts | Chart library for React | npm i recharts |
| react-hook-form | Form state management | npm i react-hook-form |
| date-fns | Date formatting and manipulation | npm i date-fns |
| axios | HTTP client (alternative to fetch) | npm i axios |
| lucide-react | Beautiful icon library for React | npm i lucide-react |
| shadcn/ui | High-quality UI component library (Tailwind) | npx shadcn@latest init |
Environment Variables & Secrets
Never hardcode API keys, database passwords, or any sensitive value directly in your code. Use environment variables — they're loaded from a separate file that's never committed to Git.
How It Works
# .env.local (in your Next.js project root)
# This file is in .gitignore — never pushed to GitHub
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://abc123.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc...
# Railway backend URL
NEXT_PUBLIC_API_URL=https://myapp.railway.app
# Server-only secrets (no NEXT_PUBLIC_ prefix)
# These are NEVER sent to the browser
DATABASE_URL=postgresql://user:password@host:5432/db
SECRET_KEY=some-random-long-string
RESEND_API_KEY=re_abc123...
Accessing Variables in Code
// Client-side (NEXT_PUBLIC_ prefix = sent to browser)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// Server-side only (API routes, server components)
const dbUrl = process.env.DATABASE_URL; // undefined in browser
const secret = process.env.SECRET_KEY;
NEXT_PUBLIC_
prefix for sensitive secrets like
DATABASE_URL
or
SECRET_KEY
. The prefix means "expose this to the browser" — anyone visiting your site can read those values in the
page source.
Where to Set Variables
| Environment | Where | Notes |
|---|---|---|
| Local dev |
.env.local
in project root
|
In .gitignore, never committed |
| Vercel (prod) | Project Settings → Environment Variables | Set for Production / Preview / Dev separately |
| Railway | Project → Variables tab | Automatically injected at runtime |
FYP Submission Checklist
Code & Repository
- GitHub repository is public (or shared with supervisor)
- README.md explains what the project does and how to run it
-
Code is clean — no commented-out debug code, no
console.logspam -
.env.localis in.gitignoreand NOT pushed to GitHub -
A
.env.examplefile exists showing which variables are needed (with dummy values) -
All features are merged into
mainbranch
Database
- All tables have correct schema (right data types, NOT NULL where needed)
- Row Level Security is enabled on Supabase tables (for production)
- Database has sample/test data so the demo looks populated
- Supabase project is not paused (send a query to keep it active)
Deployment
- App is live and accessible via public URL
- All environment variables are set in Vercel and Railway
- HTTPS works (padlock icon in browser)
- Custom domain is configured and resolving correctly
- No broken pages or API errors on the live site
Performance & Quality
- Run Lighthouse audit in Chrome DevTools — aim for green scores
- App works on mobile screen sizes
- Loading states are shown while data fetches
- Error states are handled gracefully (no blank white screens)
npm run build
locally before your final submission. If it builds without errors, Vercel will deploy successfully. Fix all
TypeScript and ESLint errors before your presentation day.
FYP Web Dev Guide · Built for Next.js + Vercel + Supabase + Railway stack
Git → React → Next.js → Tailwind → Backend → Database → Deploy