Deployment & Project Structure
A back-end on localhost helps nobody. Organise your code like a professional and put your API live on the internet.
What you will learn
- Organise an Express app into routes, controllers and models (MVC)
- Prepare an app for production (NODE_ENV, env vars, start script)
- Deploy to a hosting platform and use a Git/GitHub workflow
First, organise the code (MVC)
Every example so far lived in one file — fine for learning, but a real API with auth, validation and a dozen routes becomes an unreadable wall of code. Professionals split an Express app by responsibility, a pattern called MVC (Model–View–Controller). For an API there is no visual "View", so you focus on three folders:
| Folder | Holds | Job |
|---|---|---|
models/ | Data schemas (e.g. Task.js) | Defines the shape of your data and talks to the database |
routes/ | Route definitions (URLs + methods) | Maps each URL/method to a controller function |
controllers/ | The handler logic | Contains what actually happens for each route |
So instead of one giant file, a request flows: routes/ matches the URL, calls a function in controllers/, which uses a models/ object to read or write the database. Here is the same /tasks logic, split up:
// controllers/taskController.js — the logic
const Task = require("../models/Task");
exports.getTasks = async (req, res) => {
const tasks = await Task.find();
res.json(tasks);
};
// routes/taskRoutes.js — the URL map
const express = require("express");
const router = express.Router();
const { getTasks } = require("../controllers/taskController");
router.get("/", getTasks);
module.exports = router;
// app.js — wire the router in under a base path
app.use("/tasks", require("./routes/taskRoutes"));Reading it: the controller holds the actual work (getTasks fetches and responds). The router uses Express’s Router() — a mini-app for one resource — to say "a GET on this router runs getTasks". Finally app.use("/tasks", router) mounts the whole router under /tasks, so router.get("/") becomes GET /tasks. The single-file logic is unchanged; it is just sorted into labelled drawers, which makes a growing app navigable.
Prepare for production
Code that works on your laptop needs a few tweaks before it can run on a real server. The three essentials:
- Read the port from the environment — hosts assign one for you:
const PORT = process.env.PORT || 3000;. Never hard-code it. - Keep every secret (database URL,
JWT_SECRET) in environment variables, set in the host’s dashboard — never committed to git. - Add a
"start": "node server.js"script — hosting platforms runnpm startto launch your app.
Many libraries also behave differently in production based on NODE_ENV — a standard variable that is set to "production" on the live server. You can use it to, say, hide detailed error messages from users:
// Show full errors only while developing
app.use((err, req, res, next) => {
console.error(err); // always log the real error for yourself
const isDev = process.env.NODE_ENV !== "production";
res.status(500).json({
error: isDev ? err.message : "Something went wrong"
});
});Note: Output (in production, when a route throws):
{"error":"Something went wrong"}
Output (in development):
{"error":"Cannot read properties of undefined (reading 'name')"}
You (the developer) always get the real error in your server logs via console.error, while end users in production only see a safe, generic message — leaking internal details would help attackers.
Tip: For real logging in production, console.log is not enough. Libraries like Morgan (logs every HTTP request) or Winston/Pino (structured logs you can search and store) give you proper, timestamped records of what your server did — invaluable when debugging a live problem.
Put it live
Hosting platforms like Render, Railway or Fly.io run your Node app on a public server and give it a URL. The modern flow connects them to GitHub, so deploying becomes simply "push your code". The typical process:
- Put your project on GitHub (init a repo, commit, push) — and make sure
node_modules/and.envare in.gitignoreso they are never uploaded. - Create a new web service on the host and connect your GitHub repo.
- Set the start command to
npm startand add your environment variables (database URL,JWT_SECRET, etc.) in the host’s dashboard. - The host installs dependencies, starts your app, and gives you a public URL like
https://my-api.onrender.com. - From now on, every
git pushto your main branch triggers an automatic re-deploy — this push-to-deploy automation is the simplest form of CI/CD (Continuous Integration / Continuous Deployment).
Note: Output (after deploy): Your API is reachable at a public URL, e.g. https://my-api.onrender.com/tasks — anyone, including your React app or a recruiter, can call it. It is no longer trapped on localhost.
Watch out: Never commit your .env file or push secrets to GitHub. Anything pushed to a public repo can be scraped by bots within minutes. Set secrets in the host’s environment settings instead, and rotate any key that slips out.
Tip: Point your deployed React front-end’s fetch calls at this new public API URL (kept in a front-end env variable), and you have a complete, live, full-stack app you can put on your résumé.
Q. Why read the port with process.env.PORT instead of hard-coding 3000?
✍️ Practice
- Refactor a single-file API into
models/,routes/andcontrollers/folders, keeping the behaviour identical. - Make the app production-ready: read
process.env.PORT, add astartscript, and hide error details whenNODE_ENVis "production".
🏠 Homework
- Push an Express API to GitHub (with a proper
.gitignore) and deploy it to a free host like Render, then share the public URL of a working endpoint.