DEV Community

Cover image for Day 29 of #100DaysOfCode — Connecting Backend to Frontend
M Saad Ahmad
M Saad Ahmad

Posted on

Day 29 of #100DaysOfCode — Connecting Backend to Frontend

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
Enter fullscreen mode Exit fullscreen mode

Then, mention it in the app.js:

// app.js
import cors from "cors";
app.use(cors()); // add this before your routes
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// BACKEND — book.routes.js
router.get('/', getAllBooks);  // no auth middleware here — public
Enter fullscreen mode Exit fullscreen mode
// 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
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
// BACKEND — book.routes.js  (order matters here)
router.get('/available', getAvailableBooks);  // must be BEFORE /:id
router.get('/:id', getBookById);
Enter fullscreen mode Exit fullscreen mode
// BACKEND — book.controller.js
const getAvailableBooks = (req, res) => {
  const available = books.filter(b => b.availableCopies > 0);
  res.json(available);
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode
// BACKEND — book.routes.js
router.get('/:id', getBookById);
Enter fullscreen mode Exit fullscreen mode
// 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);
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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
};
Enter fullscreen mode Exit fullscreen mode
// 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
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode
// BACKEND — borrow.routes.js
router.post('/', checkBookAvailable, borrowBook);
//               ↑                    ↑
//          is book available?     create the borrow record
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Final Result:

See the listed books

Frontend UI


Access member details by entering the auth token

Frontend UI


Access the borrower details by entering the auth token

Frontend UI


🎉 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)