Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services

Blog Posts

Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Amine Elbarry

Amine

5+ years software engineer

~/AI_Chat~/projects~/experience~/blogs~/hire-me~/services
Back to all blogs

How to Build a Full-Stack App: A Step-by-Step Guide

Jun 20, 2026•12 min read

Building a full-stack app comes down to eight decisions made in the right order: model the data, build the API, set up the database, build the frontend, wire them together, add authentication, secure the API, and deploy. Get the order wrong and you rewrite things three times. I've shipped this exact sequence on client projects like Atikia and Lightfunnels, and it holds up whether you're building a weekend side project or an e-commerce platform handling real payments.

This is the survey. Each step here is deliberately shallow — enough to build the whole thing end to end and understand how the pieces connect — and each links to a deep dive where I go slow and show every line. We'll build a task app the whole way through: users sign in, create tasks, mark them done, delete them. Boring on purpose, because the plumbing is identical to anything more ambitious.

The architecture at a glance

Before writing code, hold the whole shape in your head. Here's the system we're building:

text
1┌─────────────┐ HTTPS/JSON ┌──────────────┐ SQL ┌──────────────┐ 2│ Browser │ ───────────────────▶ │ Node/Express │ ──────────────▶ │ PostgreSQL │ 3│ React SPA │ ◀─────────────────── │ REST API │ ◀────────────── │ database │ 4└─────────────┘ fetch / axios └──────────────┘ pg / Prisma └──────────────┘ 5 │ │ 6 │ cookie: session or JWT │ bcrypt/argon2, JWT verify 7 ▼ ▼ 8 auth state auth middleware

Three processes: a React app in the browser, a Node API on a server, and a database. They are separate programs that talk over the network. That separation is the entire game — once you internalize that the frontend and backend are two apps talking over HTTP, everything else is detail.

If you haven't picked your stack yet, I wrote a whole piece on choosing your tech stack. For this guide I'm using React + Node/Express + PostgreSQL because it's the combination I reach for most and it maps cleanly to hosting anywhere.

Step 1: Plan the data model

Start here, always. The data model is the spine of the app; every API route, every screen, and every query bends around it. Sketch your entities and their relationships before you write a single route.

For the task app, two entities:

text
1User 2 id uuid (pk) 3 email text (unique) 4 password_hash text 5 created_at timestamptz 6 7Task 8 id uuid (pk) 9 user_id uuid (fk → User.id) 10 title text 11 is_done boolean (default false) 12 created_at timestamptz

One user has many tasks; every task belongs to exactly one user. That user_id foreign key is what will later let me enforce that you can only see your tasks — a rule I'll come back to when securing the API. Spend ten minutes here drawing boxes and arrows. It saves days.

Two questions to answer for every field: is it required, and is it unique? Those answers become your NOT NULL and UNIQUE constraints, and later your validation rules.

Step 2: Build the backend API

The backend is a set of HTTP endpoints that read and write the data model. With Express, a minimal server is tiny:

js
1// server.js 2import express from 'express' 3import cors from 'cors' 4 5const app = express() 6app.use(cors({ origin: 'http://localhost:5173', credentials: true })) 7app.use(express.json()) 8 9app.get('/api/health', (req, res) => res.json({ ok: true })) 10 11app.listen(4000, () => console.log('API on :4000'))

From there, each entity gets a set of routes following REST conventions — the create/read/update/delete operations map to POST/GET/PUT/DELETE. Here's the shape for tasks:

MethodPathDoes
POST/api/tasksCreate a task
GET/api/tasksList the user's tasks
GET/api/tasks/:idGet one task
PATCH/api/tasks/:idUpdate a task
DELETE/api/tasks/:idDelete a task

This CRUD layer is the bulk of most apps, and it's worth doing deliberately. I walk through every route, status code, and validation rule in how to build a CRUD application — including why POST returns 201 and DELETE returns 204.

Step 3: Set up the database

The API needs somewhere to put the data. Translate the model from Step 1 into tables:

sql
1CREATE TABLE users ( 2 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 3 email text UNIQUE NOT NULL, 4 password_hash text NOT NULL, 5 created_at timestamptz NOT NULL DEFAULT now() 6); 7 8CREATE TABLE tasks ( 9 id uuid PRIMARY KEY DEFAULT gen_random_uuid(), 10 user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, 11 title text NOT NULL, 12 is_done boolean NOT NULL DEFAULT false, 13 created_at timestamptz NOT NULL DEFAULT now() 14); 15 16CREATE INDEX idx_tasks_user_id ON tasks(user_id);

Two things worth noticing. ON DELETE CASCADE means deleting a user cleans up their tasks automatically — the database enforces referential integrity so your app code doesn't have to. And the index on user_id matters because every task query filters by it; without the index, listing tasks does a full table scan.

Connect from Node with the pg driver:

js
1// db.js 2import { Pool } from 'pg' 3export const pool = new Pool({ connectionString: process.env.DATABASE_URL })

Then a route reads through it:

js
1app.get('/api/tasks', requireAuth, async (req, res) => { 2 const { rows } = await pool.query( 3 'SELECT id, title, is_done FROM tasks WHERE user_id = $1 ORDER BY created_at DESC', 4 [req.userId] 5 ) 6 res.json(rows) 7})

Always use parameterized queries ($1, $2) — never string-concatenate user input into SQL. That single habit closes off SQL injection, one of the oldest and most damaging vulnerability classes.

Step 4: Build the React frontend

Now the part users actually see. The frontend is a React app that fetches from the API and renders the result. A task list component is mostly state plus a fetch:

jsx
1import { useEffect, useState } from 'react' 2 3export function TaskList() { 4 const [tasks, setTasks] = useState([]) 5 6 useEffect(() => { 7 fetch('/api/tasks', { credentials: 'include' }) 8 .then((r) => r.json()) 9 .then(setTasks) 10 }, []) 11 12 return ( 13 <ul> 14 {tasks.map((t) => ( 15 <li key={t.id}>{t.is_done ? '✓' : '○'} {t.title}</li> 16 ))} 17 </ul> 18 ) 19}

The frontend holds no business logic it can be trusted on — it's a rendering layer over the API. Any rule that matters (who can see what, what's valid) gets enforced on the server, because anything in the browser can be tampered with.

Step 5: Wire the front end to the back end

Steps 2 and 4 built two programs; this step makes them talk. The React app makes HTTP requests to the Express API, and the two run on different origins in development (localhost:5173 and localhost:4000), which means you'll hit CORS. That's the browser enforcing that your API explicitly allows requests from your frontend's origin — which is why the server code back in Step 2 had app.use(cors(...)).

In development I set up a dev proxy so the frontend can call /api/tasks and Vite forwards it to the API, sidestepping CORS entirely:

js
1// vite.config.js 2export default { 3 server: { proxy: { '/api': 'http://localhost:4000' } }, 4}

The full picture — CORS, the dev proxy, project structure, and how to serve both in production — is in how to connect a React front end to a Node.js back end. It's the step people underestimate most.

Step 6: Add authentication

Right now anyone can hit /api/tasks. Authentication answers "who are you"; the app needs it before it can scope data to a user. The flow: user submits email and password, the server verifies the password against the stored hash, and issues a session.

Never store passwords in plaintext. Hash them with Argon2id or bcrypt:

js
1import argon2 from 'argon2' 2 3// on signup 4const hash = await argon2.hash(password) 5 6// on login 7const ok = await argon2.verify(user.password_hash, password) 8if (!ok) return res.status(401).json({ error: 'Invalid credentials' })

For browser apps I keep the session in an HttpOnly, Secure cookie rather than a JWT in localStorage — a token in localStorage is readable by any injected script, so one XSS bug leaks every session. The full breakdown of sessions vs JWT, OAuth for social login, passkeys, and MFA is in how to authenticate users in a web application. Get this right; it's the layer attackers probe first.

Step 7: Secure the API

Authentication proves who you are; it doesn't stop you from reading someone else's data. Once login works, an authenticated user could still request GET /api/tasks/:id for a task that isn't theirs. That's a BOLA/IDOR flaw, and it tops the OWASP API Security Top 10 for a reason. The fix is an authorization check on every object access:

js
1app.get('/api/tasks/:id', requireAuth, async (req, res) => { 2 const { rows } = await pool.query( 3 'SELECT * FROM tasks WHERE id = $1 AND user_id = $2', 4 [req.params.id, req.userId] 5 ) 6 if (!rows[0]) return res.status(404).json({ error: 'Not found' }) 7 res.json(rows[0]) 8})

That AND user_id = $2 is the whole ballgame — the database only returns the row if it belongs to the requester. Beyond object-level checks, you'll want rate limiting, input validation, safe error messages, and secret management. I turned all of it into a practical checklist for securing an API. Security is layered; no single control is enough.

Step 8: Deploy

Working on localhost is not shipped. Deployment means the frontend build served from a CDN or static host, the API running as a managed process, and the database on a managed instance with backups. The rough shape:

bash
1# build the React app to static files 2npm run build 3 4# API runs as a long-lived process, reading config from env 5NODE_ENV=production DATABASE_URL=... node server.js

The mechanics — build pipelines, environment variables, connecting to a managed Postgres, HTTPS — are covered in how to deploy a full-stack app. And once it's live and getting traffic, the questions change from "does it work" to "does it hold up": caching, load, database connection limits, and horizontal scaling. That's a discipline of its own, and I go deep on it in deploy, scale, and architect for growth.

Putting it together

Here's the whole thing as one checklist you can run down on any project:

StepDeliverableDeep dive
1. Data modelEntities, relationships, constraints—
2. Backend APIREST endpoints, Express serverCRUD app
3. DatabaseTables, indexes, foreign keysCRUD app
4. FrontendReact components fetching the APIConnect React to Node
5. Wire togetherCORS, proxy, project structureConnect React to Node
6. AuthenticationLogin, hashing, sessionsAuth guide
7. Secure the APIAuthZ, rate limits, validationSecure an API
8. DeployBuild, host, managed DB, scaleDeploy & scale

The reason this order works: each step depends on the one before it and would be painful to do out of sequence. You can't build routes without a data model, can't scope data without auth, can't secure what isn't wired together. Follow it top to bottom and you'll never find yourself ripping out foundations to fit something you should have planned in Step 1.

Build the task app once, end to end, and you've built the template for everything else. The next e-commerce store or SaaS dashboard is the same eight steps with more entities. That's not an oversimplification — it's genuinely how I approach a new client build on day one.

Related Posts

How to Authenticate Users in a Web Application

Authentication has four parts: verifying identity, managing sessions, authorizing actions, and expiring access safely. I cover password hashing, sessions vs JWT, HttpOnly cookies, OAuth, passkeys, and the architecture I actually recommend.

How to Connect a React Front End to a Node.js Back End

A React app and a Node.js API are two separate programs talking over HTTP. I show the Express server, the React fetch, why CORS exists, how to set up a dev proxy, project structure, and how to run and deploy both.

How to Secure an API: A Practical Checklist

A layered, defense-in-depth checklist for securing an API — HTTPS, auth, RBAC and object-level checks against BOLA/IDOR, rate limiting, input validation, safe errors, logging, secret management, and the OWASP API Security Top 10. Plus the myths that get people breached.