Day 2 of the from-zero series. Build a complete REST API from scratch — one step at a time.
A Product CRUD REST API with:
- Node.js as the runtime (like JVM for Java)
- Express as the web framework (like Spring Boot for Java)
- MySQL as the database
- mysql2 as the database driver (with async/await support)
- Layered architecture: Routes → Controller → Service → Model → Database
By the end, you'll have 5 working endpoints that can Create, Read, Update, and Delete products.
| Tool | Version | Check Command | Install From |
|---|---|---|---|
| Node.js | 18+ | node --version |
https://nodejs.org/ |
| npm | 9+ | npm --version |
(comes with Node.js) |
| MySQL | 5.7+ | mysql --version |
https://dev.mysql.com/ or XAMPP |
Make sure MySQL is running before you start (e.g., start XAMPP → MySQL).
This project was built in 8 steps, each as a separate git commit.
# See all steps in order (oldest first)
git log --oneline --reverse
# See exactly what changed in each step
git show <commit-hash>
# Or see a specific step's diff
git log --oneline --reverse # find the hash
git show abc1234 # view that step's changes# 1. Clone the repo
git clone https://github.com/dev48v/nodejs-from-zero.git
cd nodejs-from-zero
# 2. Install dependencies
npm install
# 3. Create your .env file (copy the example)
cp .env.example .env
# Edit .env if your MySQL user/password is different from root/(empty)
# 4. Start the server (it auto-creates the database and table)
npm start
# You should see:
# > Database initialized successfully
# > Server is running on http://localhost:3000curl http://localhost:3000/mysql -u root nodejs_from_zero_db < seed.sqlcurl http://localhost:3000/api/productscurl http://localhost:3000/api/products/1curl -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json" \
-d '{"name": "Wireless Earbuds", "description": "Bluetooth 5.3, 30hr battery", "price": 59.99, "quantity": 50}'curl -X PUT http://localhost:3000/api/products/1 \
-H "Content-Type: application/json" \
-d '{"name": "MacBook Pro 16\" (Updated)", "description": "M3 Max chip, 36GB RAM", "price": 3499.99, "quantity": 10}'curl -X DELETE http://localhost:3000/api/products/1
# Returns 204 No Content (empty response = success)nodejs-from-zero/
├── .env.example ← Environment variable template
├── .gitignore ← Ignores node_modules, .env, logs
├── package.json ← Dependencies and scripts
├── seed.sql ← Sample data (5 products) [STEP 8]
├── GUIDE.md ← This file
├── README.md ← Same as GUIDE.md
└── src/
├── app.js ← Entry point: Express setup [STEP 1]
├── config/
│ └── database.js ← MySQL pool + auto-init [STEP 2]
├── models/
│ └── Product.js ← Raw SQL CRUD queries [STEP 3]
├── dto/
│ └── ProductDTO.js ← DB row ↔ API response mapping [STEP 4]
├── services/
│ └── ProductService.js ← Business logic + validation [STEP 5]
├── controllers/
│ └── ProductController.js ← HTTP handlers (req/res) [STEP 6]
├── routes/
│ └── productRoutes.js ← URL → Controller wiring [STEP 7]
└── middleware/
└── errorHandler.js ← Global error middleware [STEP 8]
Client (curl / Postman / browser)
│
▼
┌─────────────────────────────────────────────────┐
│ app.js │
│ ├── express.json() middleware (parse body) │
│ ├── GET / (health check) │
│ ├── /api/products → productRoutes.js │
│ └── errorHandler middleware (catch all errors) │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ productRoutes.js │
│ Maps HTTP method + path → controller function │
│ GET / → getAll │
│ GET /:id → getById │
│ POST / → create │
│ PUT /:id → update │
│ DELETE /:id → remove │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ ProductController.js │
│ Reads req (params, body) → calls service │
│ Sends res (status code, JSON body) │
│ Passes errors to next() → errorHandler │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ ProductService.js │
│ Validates input → calls model → maps with DTO │
│ Throws errors with statusCode (400, 404) │
└─────────────────────────────────────────────────┘
│
▼
┌──────────────────┐ ┌─────────────────────────┐
│ ProductDTO.js │ │ Product.js (Model) │
│ toDto() │ │ findAll(), findById(), │
│ toEntity() │ │ create(), update(), │
│ toDtoList() │ │ remove() │
└──────────────────┘ └─────────────────────────┘
│
▼
┌───────────┐
│ MySQL DB │
│ (pool) │
└───────────┘
| Package | Why |
|---|---|
| express | Web framework — handles routing, middleware, HTTP requests/responses |
| mysql2 | MySQL driver with promise/async-await support (faster than mysql pkg) |
| dotenv | Loads .env file into process.env — keeps secrets out of code |
| Concept | Spring Boot (Java) | Express (Node.js) |
|---|---|---|
| Entry point | @SpringBootApplication class |
app.js with app.listen() |
| Dependency injection | @Autowired / constructor inject |
require() / import |
| ORM | JPA/Hibernate (auto-maps) | Raw SQL with mysql2 (manual) |
| Annotations | @GetMapping, @Entity, etc. |
router.get(), plain objects |
| Error handling | @ControllerAdvice |
app.use((err, req, res, next)) middleware |
| Config | application.properties |
.env + dotenv |
| Build tool | Maven / Gradle | npm |
| Hot reload | Spring DevTools | node --watch (built-in) |
Once you're comfortable with this project, try:
- Add pagination —
GET /api/products?page=1&limit=10 - Add search/filter —
GET /api/products?name=laptop&minPrice=100 - Use an ORM — Replace raw SQL with Sequelize or Prisma
- Add validation — Use a library like
joiorexpress-validator - Add tests — Use Jest + Supertest for API testing
- Dockerize — Create a Dockerfile so anyone can run this with
docker compose up