What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Folder structure feels unimportant until the project hits fifty files and nobody can find anything. Layer-based vs feature-based, monorepo or not, and a layout that scales calmly.
Every project starts tidy. Ten files, obvious names, everything fits on one screen. Six months later there's a utils folder with 40 unrelated files, three different places that call the API, and a new teammate asking where the login logic lives. Folder structure doesn't matter on day one. It matters a lot on day two hundred.
Here's how to structure a full stack project so it stays easy to work in as it grows.
Layer-based (group by technical role):
src/
components/
hooks/
services/
utils/
pages/
Simple and familiar. It works well for small apps. The problem shows up at scale: one feature, say "invoices," ends up spread across five folders, and every change means jumping around the tree.
Feature-based (group by what the code does for the user):
src/
features/
auth/
LoginForm.jsx
useSession.js
auth.api.js
invoices/
InvoiceList.jsx
InvoiceForm.jsx
invoices.api.js
invoices.test.js
shared/
ui/
lib/
Everything about invoices lives together. You can understand, change or delete a feature by looking at one folder.
The practical answer: start layer-based if the app is tiny, then move to feature-based once you have more than a handful of distinct features. Most real projects end up as a hybrid: features for domain code, plus a shared area for truly generic pieces.
For a single repo with a frontend and a backend:
my-app/
apps/
web/ # frontend (React, Next.js, Vue...)
api/ # backend (Express, Fastify, Django...)
packages/
shared/ # types, validation schemas, constants used by both
database/
migrations/
seeds/
.github/workflows/
docker-compose.yml
.env.example
README.md
If you use a full stack framework like Next.js, Nuxt or SvelteKit, frontend and backend live in one app, and the feature-based idea applies inside it:
src/
app/ # routes / pages
features/ # domain logic grouped by feature
components/ui/ # generic, reusable UI
lib/ # db client, auth helpers, config
1. Keep the database and external APIs behind one layer.
Components shouldn't write SQL or call third-party APIs directly. Put that in a data or service layer (lib/db, features/invoices/invoices.server.js). When you swap providers or add caching, you change one place.
2. Share validation between client and server. Define a schema once (with Zod, Valibot, Yup or similar) and use it for both form validation and API input validation. The frontend gets instant feedback; the backend still enforces it.
3. Make "shared" earn its place.
Code goes into shared only when at least two features genuinely use it. Premature sharing creates a junk drawer that everything depends on.
4. Keep config in one module. Read environment variables in one file, validate them, export a typed object. (Here's the pattern.)
5. Put tests next to the code they test.
invoices.test.js beside invoices.js gets updated. A distant tests/ mirror tree tends to rot.
6. Name things for what they are.
formatCurrency.js beats helpers2.js. If you can't name a file clearly, it's probably doing too many things.
A monorepo (one repository, multiple apps and packages) shines when the frontend and backend share types and change together. One pull request can update the API and the UI at once. Tools like npm/pnpm workspaces, Turborepo or Nx help with builds and caching.
Separate repos make sense when different teams own different services, or when they deploy on completely independent schedules.
For a solo developer or small team building one product, a monorepo is usually less friction. Don't introduce heavy tooling until build times actually hurt.
utils or helpers has become the biggest folder in the project.Refactoring structure doesn't need to be a big-bang rewrite. Move one feature into its own folder, see if it feels better, then move the next.
npm run dev or docker compose up (Docker basics here).For small apps, a simple components, hooks, pages split is fine. As the app grows, group code by feature (features/auth, features/billing) with a small shared folder for generic UI and utilities.
For most small and medium projects, yes. A monorepo keeps shared types in sync and lets one change update both sides. Split them when teams or deployment schedules are truly independent.
Group by feature or resource (users, orders), each with its routes, service logic and data access, and keep app-wide middleware, config and the database client in a shared lib folder.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →