In this guide
Three weeks into your web project, you cannot find the login code. It might be in server.js, or in routes.js, or in the file you named final2.js. The frontend calls an API address that only works on your laptop. Your teammate pushed a database password to GitHub. Nothing is broken yet, but every new feature takes twice as long as the last one, because the project has no shape.
The shape of a full-stack project is decided in the first hour, and it is the cheapest decision you will ever make: where each file lives, what each folder is allowed to contain, and where the line sits between the frontend, the backend, and the database. This guide shows a structure that works for a student MERN project — MongoDB, Express, React, Node — with a complete worked tree, the rules that keep it clean, and the mistakes that turn it into a mess.
Quick answer
A full-stack web project has three parts — a frontend people see, a backend API that enforces the rules, and a database that keeps the data. Structure it as one folder per part, with the backend split into routes, controllers, services, and models. Keep secrets in environment variables, never in code, and give each side its own package file.
| Layout | Best for | Watch out for |
|---|---|---|
| Monorepo (frontend + backend folders) | Student teams, shared types, one repo | Git history mixes both sides |
| Two repos (polyrepo) | Separate deploys, larger teams | A change across both needs two commits |
| Single flat folder | A weekend prototype only | Becomes unfindable past a dozen files |
The three rules underneath every good structure
Before the tree, the rules. Every folder layout that survives a semester follows these three, and every one that collapses breaks at least one.
1. Separation of concerns. Each part of the system does one job. The frontend renders and collects input. The backend validates, enforces rules, and talks to the database. The database stores. When the frontend starts checking passwords, or the backend starts building HTML strings, the jobs have leaked, and the next change touches three places at once.
2. One job per folder. A folder's name should tell you what is inside it without opening it. routes/ holds route definitions. models/ holds data shapes. components/ holds reusable UI. If you open a folder and find three different kinds of file, the folder is lying about its job — split it.
3. Boundaries are one-way. The frontend may call the backend's API. The backend may read the database. The frontend must never import backend code or read the database directly, and a backend route handler must never contain business logic that belongs in a service. One-way boundaries mean you can rewrite one side without touching the other.
The full tree: a worked example
Here is the complete structure for a campus complaint portal — students file complaints, staff update their status, admins see a dashboard — built with the MERN stack. This is a real shape you can copy, not a sketch:
complaint-portal/
├── frontend/ # React app (Vite)
│ ├── public/ # Favicon, static files
│ ├── src/
│ │ ├── api/ # API client functions (one file per resource)
│ │ │ ├── complaints.js
│ │ │ └── auth.js
│ │ ├── components/ # Reusable UI: Button, ComplaintCard, StatusBadge
│ │ ├── pages/ # One folder per screen: Login, Dashboard, NewComplaint
│ │ ├── hooks/ # Shared React hooks: useAuth, useComplaints
│ │ ├── context/ # App-wide state: AuthContext
│ │ ├── utils/ # Small helpers: formatting dates, validation
│ │ ├── App.jsx
│ │ └── main.jsx
│ ├── .env.example # Placeholder variables, no real values
│ └── package.json # Frontend dependencies only
├── backend/ # Node.js + Express API
│ ├── src/
│ │ ├── config/ # Database connection, environment loading
│ │ │ └── database.js
│ │ ├── routes/ # URL definitions only, no logic
│ │ │ ├── complaints.routes.js
│ │ │ └── auth.routes.js
│ │ ├── controllers/ # Request handlers: read input, call service, send reply
│ │ │ ├── complaints.controller.js
│ │ │ └── auth.controller.js
│ │ ├── services/ # Business logic: rules, checks, calculations
│ │ │ ├── complaints.service.js
│ │ │ └── auth.service.js
│ │ ├── models/ # Database schemas (Mongoose)
│ │ │ ├── Complaint.js
│ │ │ └── User.js
│ │ ├── middleware/ # Auth check, error handler, validation
│ │ │ ├── auth.js
│ │ │ └── errorHandler.js
│ │ ├── utils/ # Small helpers: tokens, email
│ │ └── app.js # Express setup: middleware, routes
│ ├── tests/ # API tests, one file per route group
│ ├── .env.example # Placeholder variables, no real values
│ └── package.json # Backend dependencies only
├── database/ # Scripts, not application code
│ ├── migrations/ # Schema changes, in order
│ └── seed.js # Demo data for development only
├── docs/ # Project report material, API notes, demo links
├── .gitignore # node_modules, .env, build output
├── README.md # Setup steps, how to run, who does what
└── package.json # Root scripts only: install-all, dev, start
Three things to notice. First, the frontend and backend each have their own package.json, so each side installs and runs independently — one side's dependency problem never blocks the other. Second, the database has no application code in it, only scripts: migrations and seed data live apart from the running server. Third, the root holds almost nothing — a README, a gitignore, and scripts that start both sides together. A root folder full of application code is a sign the split never happened.
The frontend, layer by layer
pages/ — one folder per screen. Login, Dashboard, NewComplaint, ComplaintDetail. A page assembles components and calls hooks; it holds no API code and no business rules. If a screen grows past a few hundred lines, the reusable parts belong in components/, not inline.
components/ — reusable UI only. A button, a complaint card, a status badge. A component takes data in and renders it out; it does not fetch data and it does not know about the backend. If you use a piece of UI twice, it belongs here.
api/ — every backend call in one place. One file per resource: complaints.js holds the functions that list, create, and update complaints; auth.js holds login and logout. No page and no component ever calls the network directly — they call these functions. When the API address changes, you change it in one file, not in twenty.
hooks/ and context/ — shared state. useAuth knows who is logged in; AuthContext carries that knowledge to every page. State that only one page needs stays in that page. State that three pages need moves up here.
utils/ — small pure helpers. Formatting a date, checking an email address shape, turning a status code into a label. No network calls, no state — helpers you could test with no setup at all.
The backend, layer by layer
The backend follows a chain, and every request walks it in the same order: route → controller → service → model.
routes/ — URL definitions only. A route file says "a POST to /complaints goes to the create-complaint controller, and it must pass the auth check first." Nothing else. No database calls, no validation logic, no calculations. A route file you can read in thirty seconds is a route file doing its job.
controllers/ — thin request handlers. A controller reads the request, calls the right service function, and sends the reply. It converts between HTTP and the rest of the system — status codes, JSON shapes, error responses — and it contains no business rules. The REST API design guide covers how to shape these endpoints well.
services/ — where the rules live. Can a student edit a complaint after staff have replied? Can a staff member close a complaint they did not take? Those rules live in the service, in plain functions you can test without starting a server. This is the layer that makes the project testable: a service function takes inputs and returns a result, with no HTTP and no database connection in the way.
models/ — data shapes. A Mongoose schema says a complaint has a title, a description, a status, a student, and timestamps. The model is the only layer that talks to the database directly. If you change databases later, this is the layer that changes.
middleware/ — checks that run before the handler. The auth check (is this request logged in?), the error handler (turn a crash into a clean error reply), and input validation. Middleware keeps the controllers short by removing the checks every handler would otherwise repeat.
config/ — connections and environment. The database connection lives here, reading its address from an environment variable — never a password written in code. This is also where the server reads its port and its running mode.
Config and secrets: the file that must never leak
Every full-stack project needs values that change between machines: the database address, the API address, a signing key. Those values live in environment files, and the rule is simple — the real file never enters version control, and a placeholder file shows the shape:
# backend/.env.example — committed to Git, safe to share
MONGODB_URI=mongodb://localhost:27017/complaint-portal
PORT=5000
JWT_SECRET=<generate-a-long-random-value>
# backend/.env — real values, NEVER committed
# (this file is listed in .gitignore)
The .env file is listed in .gitignore from the first commit, before any real value exists — because the leak always happens on the commit where someone adds the secret "just for now". The .env.example file documents which variables the project needs, with placeholder values, so a teammate can set up without asking you. The environment variables guide covers this pattern in full, including what to do if a secret has already been committed.
Tests, docs, and the report
tests/ sits beside the backend source, one file per route group. Test the service layer first — it holds the rules, and it runs without a server or a database. Then test the routes that matter most: login, creating a complaint, changing a status. A project with tests for its rules is a project you can change without fear, which is exactly what a semester of feedback requires.
docs/ is where the report material lives: the API notes, the database diagram, the demo video link, and the screenshots. Keeping these in the repo means the report is written from the real system, not from memory the night before submission. The Git branching guide covers how a team keeps this folder tidy when four people are committing at once.
Deploying the split
In development the frontend and backend run on two different ports on your laptop, and the frontend calls localhost. In production they are two separate deployments, and the frontend must call the deployed API address — the single most common deployment bug in student projects is a frontend that still calls localhost after deployment and works on nobody's machine but the author's.
The clean split: the frontend is static files, deployed to a static host; the backend is a server, deployed to a VPS or a platform service; the database is a managed service or a database on the same server. Each side gets its own environment variables on its own host — the frontend gets the API address, the backend gets the database address and the signing key. The MERN deployment guide walks through the VPS side of this, and the Docker Compose guide covers running both sides together in containers for a demo.
Common mistakes
- Fat routes. A route handler that validates input, checks permissions, queries the database, and formats the reply is doing four jobs. Thin routes, logic in services — the chain exists so each layer stays small.
- Secrets in the repo. A database password or signing key committed to Git is compromised the moment it is pushed, even to a private repo. Environment variables, gitignore from day one, and a placeholder
.env.example. - Frontend calling localhost in production. The deployed frontend must call the deployed API address, read from its own environment variable — never a hardcoded
localhostaddress. - One package.json for two apps. Frontend and backend have different dependencies and different start commands. Sharing one package file means installing the world to run either side, and a version clash on one side breaks both.
- Business logic in the controller. Rules written inside a request handler cannot be tested without a server and cannot be reused by another endpoint. Services hold the rules; controllers translate HTTP.
- No README. A project nobody else can run is a project nobody else can help with. The README states how to install, how to run each side, which environment variables exist, and who owns what.
- Flat folders past a dozen files. A single folder holding every file works for a weekend prototype. Past a dozen files, finding anything costs more time than the structure ever would.
Frequently asked questions
Monorepo or two repos for a student team?
One repo with frontend/ and backend/ folders — a monorepo — for almost every student project. It keeps the whole system in one place, one clone sets up everything, and shared types or constants live in one spot. Two repos only earn their complexity when the two sides deploy on different schedules with different teams.
Where does the database fit in the tree?
The database itself is not code, so it has no folder in the application. What lives in the repo is the shape of the data — the models — and the scripts that set it up: migrations and seed data in a database/ folder. The choice between SQL and NoSQL shapes those models; the SQL vs NoSQL guide covers how to decide.
Should the frontend and backend share code?
Only the genuinely shared pieces: validation rules, status constants, date formatting. In a monorepo those live in a shared/ or packages/ folder both sides import from. Shared code that one side changes without telling the other is worse than duplicated code — keep the shared surface small.
How do I keep the frontend and backend in sync?
Through the API contract: the endpoints, the request shapes, and the response shapes. Write them down in docs/ or as a small API reference, and treat a change to the contract as a change both sides must agree on. The API client in frontend/src/api/ is the frontend's copy of that contract.
Where do I put files the user uploads?
Not in the repo and not in the frontend folder. Uploads go to a storage folder on the server or an object-storage service, with only the file path or URL stored in the database. Committing uploaded files bloats the repo and leaks user data into version control.
What if my project is not MERN?
The shape survives the stack. A Django project has apps instead of services, a Spring Boot project has controllers and services by the same names, a Flask project splits blueprints the same way. The layers — routes, handlers, rules, data — are the same in every backend; only the folder names change.
Limitations
This guide describes one proven structure for a MERN student project; it is a starting point, not a standard. Larger systems split further — separate services, a dedicated API gateway, feature-based folders — and a team that outgrows this tree should split the layer that hurts, not redesign everything at once. Framework conventions override this guide where they exist: a Next.js project has opinions about where pages live, and fighting the framework costs more than the tidy tree saves. Environment and deployment details change with the hosting choice; the deployment guides linked above carry the specifics for a VPS and for containers.
Where to go from here
- REST API Design Principles for Students — shape the endpoints your routes expose.
- How to Deploy a MERN App on a VPS — take the split structure live.
- Docker Compose for Full-Stack Projects — run frontend, backend, and database together for a demo.
- SQL vs NoSQL: Which Database for Your Project — choose the database your models will describe.
- How to Debug a Full-Stack Web App — when the frontend and backend disagree, this is where to look.
- GitHub Actions CI/CD Pipeline Guide — test both sides on every push.
- More web development material in the Web Development branch hub.
Building a full-stack project and want the structure, the API, and the deployment set up correctly from the start? Message Projectech on WhatsApp (wa.me/918275719193) with your stack, your feature list, and your deadline — you will get a quotation stating the scope, the price, and the delivery timeline for the built-to-order project.