For today, Day 29, I built the frontend UI of the Library Management System using React and TailwindCSS, and connected it to the backend I built yesterday.
Here’s the complete breakdown of how the frontend communicates with the backend, how requests travel through middlewares, and how each UI action maps to backend logic.
🔗 Yesterday’s backend article:
Day 28 — Building a Library API
Enabling Backend ↔ Frontend Communication With CORS
To make the frontend talk with the backend, we need to install cors and mention it in the app.js
First, install cors:
npm install cors
Then, mention it in the app.js:
// app.js
import cors from "cors";
app.use(cors()); // add this before your routes
This ensures the browser doesn’t block API requests.
1. Fetch the request — fetchApi
Every single request goes through this one function in the frontend:
// App.jsx
const fetchAPI = async (path, options = {}, token = "") => {
const headers = { "Content-Type": "application/json" };
if (token) headers["authorization"] = token;
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
const data = await res.json();
return { ok: res.ok, status: res.status, data };
};
On the backend, app.js is the receiving end. Every request hits these in order before reaching any route:
// BACKEND — app.js
app.use(express.json()) // parses the JSON body frontend sends
app.use(cors()) // allows the browser to talk to this server
app.use(logger) // logs every incoming request
Without express.json(), your req.body would always be undefined.
2. Auth Token — Frontend input → Auth Middleware
When you type librariantoken in the UI and click Set, it gets stored in state. Then every protected request passes it:
// FRONTEND — fetchAPI call for a protected route
await fetchAPI("/books", { method: "POST", body: JSON.stringify(form) }, token);
// this puts: headers["authorization"] = "librariantoken"
That header travels to the backend and hits auth.middleware.js:
// BACKEND — auth.middleware.js
const auth = (req, res, next) => {
const token = req.headers['authorization']; // reads what frontend sent
if (!token) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
if (token !== VALID_TOKEN) { // VALID_TOKEN = 'librariantoken'
return res.status(403).json({ error: 'Invalid token.' });
}
next(); // token is valid → move to the controller
};
If you don't set a token in the UI and try to add a book, the frontend gets back { error: 'Access denied' } and the StatusBadge shows it in red. That red badge is literally the res.status(401).json(...) response from the middleware.
3. GET /books — Public Route, Query Params, Available Filter
Case 1 — Load all books:
// FRONTEND — BooksSection
const { ok, data } = await fetchAPI("/books"); // no token passed
// BACKEND — book.routes.js
router.get('/', getAllBooks); // no auth middleware here — public
// BACKEND — book.controller.js
const getAllBooks = (req, res) => {
const { genre } = req.query; // checks for ?genre=
if (genre) {
const filtered = books.filter(b => b.genre.toLowerCase() === genre.toLowerCase());
return res.json(filtered);
}
res.json(books); // returns the full array from books.data.js
};
The data that comes back is that array — the frontend stores it in setBooks(data) and .map() renders each row in the table.
Case 2 — Genre filter checkbox:
// FRONTEND
let path = "/books";
if (genreFilter) path += `?genre=${encodeURIComponent(genreFilter)}`;
// becomes: /books?genre=Classic
That ?genre=Classic part becomes req.query.genre on the backend — that's the direct connection between the input box and req.query.
Case 3 — Available only checkbox:
// FRONTEND
let path = availableOnly ? "/books/available" : "/books";
// BACKEND — book.routes.js (order matters here)
router.get('/available', getAvailableBooks); // must be BEFORE /:id
router.get('/:id', getBookById);
// BACKEND — book.controller.js
const getAvailableBooks = (req, res) => {
const available = books.filter(b => b.availableCopies > 0);
res.json(available);
};
4. GET /books/:id — Route Params
When you click the View button on any book row:
// FRONTEND
const fetchOne = async (id) => {
const { ok, data } = await fetchAPI(`/books/${id}`);
// e.g. /books/3
};
// BACKEND — book.routes.js
router.get('/:id', getBookById);
// BACKEND — book.controller.js
const getBookById = (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
// req.params.id is the "3" from /books/3
if (!book) return res.status(404).json({ error: 'Book not found' });
res.json(book);
};
The id from the frontend URL slot becomes req.params.id on the backend.
5. POST /books — Body + Validation Middleware Chain
When you fill the Add Book form and click Add:
// FRONTEND
const form = { title: "1984", author: "Orwell", genre: "Dystopian", copies: 3 }
await fetchAPI("/books", { method: "POST", body: JSON.stringify(form) }, token);
The backend runs 3 things in sequence before the controller:
// BACKEND — book.routes.js
router.post('/', auth, validateBook, createBook);
// ↑ ↑ ↑
// check token check body actually create
// BACKEND — validateBook.middleware.js
const validateBook = (req, res, next) => {
const { title, author, genre, copies } = req.body;
// req.body is exactly the object the frontend sent
if (!title || title.trim() === '') {
return res.status(400).json({ error: 'Book title is required' });
}
// ... other checks
next(); // all good → go to createBook
};
// BACKEND — book.controller.js
const createBook = (req, res) => {
const { title, author, genre, copies } = req.body;
const newBook = {
id: books.length + 1,
title, author, genre,
totalCopies: parseInt(copies),
availableCopies: parseInt(copies),
};
books.push(newBook);
res.status(201).json(newBook); // frontend receives this
};
The 201 response makes res.ok true in the frontend, the status badge goes green, and loadBooks() is called again to re-fetch and re-render the updated table.
6. Borrow a Book — Two Middlewares in Chain
// FRONTEND
await fetchAPI("/borrows", {
method: "POST",
body: JSON.stringify({ memberId: 1, bookId: 3 })
}, token);
// BACKEND — borrow.routes.js
router.post('/', checkBookAvailable, borrowBook);
// ↑ ↑
// is book available? create the borrow record
checkBookAvailable reads req.body.bookId, finds the book, checks availableCopies > 0, and if all good, attaches the book object to the request:
// BACKEND — checkBookAvailable.middleware.js
req.book = book; // passes the found book forward
next();
Then borrowBook controller uses that attached object directly — no second lookup needed:
// BACKEND — borrow.controller.js
req.book.availableCopies -= 1; // modifies the book that middleware found
This is the middleware "passing data forward" pattern — the frontend triggers it with one request body, but two backend functions work on it in sequence.
The Full Flow Visualized
Frontend click
↓
fetchAPI("/borrows", POST, { memberId, bookId }, token)
↓
app.js: express.json() → parses body
app.js: cors() → allows request
app.js: logger → logs it
↓
borrow.routes.js: router.use(auth) → checks token header
↓
borrow.routes.js: checkBookAvailable → checks copies, attaches req.book
↓
borrow.controller.js: borrowBook → creates record, sends res.status(201)
↓
fetchAPI receives { ok: true, data: { message, borrow, dueDate } }
↓
setStatus({ ok: true, message: data.message }) → green badge renders
loadBorrows() → re-fetches table
Final Result:
See the listed books
Access member details by entering the auth token
Access the borrower details by entering the auth token
🎉 Wrap Up
Today’s session was all about understanding how the frontend and backend actually talk to each other:
- How headers map to middlewares
- How body data becomes
req.body - How routes decode URL params
- How query strings become
req.query - How middleware chains work in real apps
Thanks for reading. Feel free to share thoughts. The journey continues tomorrow.



Top comments (0)