Case Study: Bookmark Manager

circle-info

Follow along with code examples herearrow-up-right!

Setup

cd server
npm i
npm run dev

The server will be running at http://localhost:8080arrow-up-right.

Overview

This case study application is a full-stack Bookmark Manager built with Express (backend) and vanilla JavaScript (frontend). Users can view their saved bookmarks, add new ones via a form, and delete them.

This case study demonstrates Express server setup, REST API design, the MVC (Model-View-Controller) architecture pattern, and connecting a frontend to a locally hosted backend via fetch.

Controllers use the Fellow Model interface to update the "database" before sending a response back to the client.

The completed solution files are:

  • server/index.js — Express server with middleware and routes

  • server/models/bookmarkModel.js — In-memory data model

  • server/controllers/bookmarkControllers.js — Route handler functions

  • frontend/index.html — The HTML structure

  • frontend/src/fetch-helpers.js — Functions that call the API

  • frontend/src/dom-helpers.js — Functions that update the DOM

  • frontend/src/main.js — Page load logic and event handlers

Explore the Solution

Trace the Flow

For each scenario below, trace the path through the code across files. In order of execution, write down the sequence of function calls:

  • which file and function was involved

  • a brief description of what it does

  • what was returned or sent

An example is provided for the first scenario.

Scenario 1: The page loads and bookmarks are rendered on the screen

  1. frontend/src/main.js: main() is called on page load.

  2. frontend/src/main.js: await getBookmarks() is called.

  3. frontend/src/fetch-helpers.js: getBookmarks() sends a GET /api/bookmarks request to the server.

  4. server/index.js: The logRoutes middleware logs the request, then app.get('/api/bookmarks', listBookmarks) matches and calls listBookmarks.

  5. server/controllers/bookmarkControllers.js: listBookmarks() calls bookmarkModel.list().

  6. server/models/bookmarkModel.js: bookmarkModel.list() returns a shallow copy of the in-memory bookmarks array.

  7. server/controllers/bookmarkControllers.js: res.send(bookmarks) sends the array as JSON to the client.

  8. frontend/src/fetch-helpers.js: response.json() parses the JSON and the array is returned.

  9. frontend/src/main.js: renderBookmarks(bookmarks) is called with the resolved array.

  10. frontend/src/dom-helpers.js: renderBookmarks() clears the list, updates the count, creates <li> elements with links and delete buttons, and appends them to #bookmarks-list.

Scenario 2: The user fills out the form and submits a new bookmark

chevron-rightAnswerhashtag
  1. frontend/src/main.js: The submit event fires on #bookmark-form, calling handleFormSubmit.

  2. frontend/src/main.js: The title and url are read from form.title.value and form.url.value and await createBookmark(title, url) is called.

  3. frontend/src/fetch-helpers.js: createBookmark() sends a POST /api/bookmarks request with Content-Type: application/json and the bookmark data stringified in the request body.

  4. server/index.js: express.json() middleware parses the request body into req.body. app.post('/api/bookmarks', createBookmark) matches and calls the createBookmark controller.

  5. server/controllers/bookmarkControllers.js: The controller validates title and url, calls bookmarkModel.create(title, url).

  6. server/models/bookmarkModel.js: bookmarkModel.create() generates a new id, pushes the new bookmark into the array, and returns it.

  7. server/controllers/bookmarkControllers.js: The controller adds a createdAt timestamp to the new bookmark (which should happen in the model) and and sends a 201 response with the new bookmark.

  8. frontend/src/main.js: await getBookmarks() is called to retrieve the full updated list.

  9. frontend/src/main.js: renderBookmarks(updated) re-renders all bookmarks.

Scenario 3: The user clicks the Delete button on a bookmark

chevron-rightAnswerhashtag
  1. frontend/src/main.js: A click event fires on #bookmarks-list, calling handleDeleteBookmarkClick.

  2. frontend/src/main.js: handleDeleteBookmarkClick gets the bookmark's id from the clicked button's data-bookmark-id attribute and calls await deleteBookmark(clickedBtn.dataset.bookmarkId).

  3. frontend/src/fetch-helpers.js: deleteBookmark() sends a DELETE /api/bookmarks/:id request to the server.

  4. server/index.js: app.delete('/api/bookmarks/:id', deleteBookmark) matches and calls the deleteBookmark controller.

  5. server/controllers/bookmarkControllers.js: The controller calls bookmarkModel.destroy(Number(id)).

  6. server/models/bookmarkModel.js: bookmarkModel.destroy() finds the bookmark by index, removes it with splice, and returns true. Returns false if not found.

  7. server/controllers/bookmarkControllers.js: res.sendStatus(204) sends an empty 204 No Content response.

  8. frontend/src/main.js: await getBookmarks() re-fetches the updated list, then renderBookmarks(updated) re-renders.


Guided Reading Questions

Open each file and answer the questions.

server/index.js

  1. What does express.json() middleware do, and why is it needed?

  2. What does express.static(pathToFrontend) do? What is the __dirname variable's value and how is it used to construct the final pathToFrontend value? (console log both __dirname and pathToFrontend to find out)

  3. What does the logRoutes middleware do, and what happens if you remove the next() call?

  4. With the serving running, use curl to test each endpoint below. For each, record the status code, the terminal log printed by the server, and a brief description of the response:

circle-info

A file test-requests.sh has been created for you with these curl requests. It can be executed from the root of the repository with the command:

Files like these prevent you from needing to type out the entire command directly in the terminal!

chevron-rightAnswershashtag
  1. express.json() parses incoming requests with a JSON body and attaches the result to req.body. Without it, req.body would be undefined when a client sends JSON (e.g., on POST or PATCH requests).

  2. express.static() serves all files in a given folder as static assets. __dirname is a Node.js variable that holds the absolute path to the directory of the current file — in this case, the swe-casestudy-5/server/ folder. path.join(__dirname, '../frontend') navigates one level up and into frontend/ to create the absolute path to the frontend/ folder which is stored in pathToFrontend. Visiting http://localhost:8080 then delivers frontend/index.html automatically.

  3. logRoutes logs the HTTP method, URL, and timestamp for every incoming request. If next() is removed the middleware would never pass control to the next handler in the chain and since logRoutes doesn't send a response itself, the request would "hang" (the client would be waiting forever).

  4. Sample curl commands and responses:

server/models/bookmarkModel.js

  1. Where is the "database" stored? What are its limitations compared to a real database? What happens to the bookmark data if you restart the server?

  2. Why does bookmarkModel.list() return [...bookmarks] instead of just bookmarks? Why do find and update return { ...bookmark } instead of bookmark? How does this relate to separation of concerns?

  3. What does bookmarkModel.update() return if no bookmark matches the id? What does bookmarkModel.destroy() return if no match is found? The data types of these two return values are different—what justification can you provide to explain this difference?

chevron-rightAnswershashtag
  1. The bookmarks are stored in an in-memory JavaScript array (const bookmarks = [...]). Limitations: changes to the data are lost on server restart. All bookmark data resets to the three hardcoded initial values each time the module is reloaded.

  2. [...bookmarks] returns a shallow copy of the array so callers can't accidentally mutate the internal store by modifying the returned reference. { ...bookmark } does the same for individual objects — it returns a copy so callers cannot mutate the stored record directly. This creates a safe interface that encapsulates the bookmarks data and creates clearer separation of concerns.

  3. bookmarkModel.update() returns null explicitly when no bookmark matches. bookmarkModel.destroy() returns false when no matching bookmark is found. Each of these return values complement the value returned when the operation is successful. bookmarkModel.update() returns an object when successful and null is the absence of a valid object. Meanwhile bookmarkModel.destroy() returns true and false is the opposite/absence of a true value.

server/controllers/bookmarkControllers.js

  1. Why does getBookmark call Number(id) when id comes from req.params?

  2. What HTTP status code does createBookmark send on success, and why is 201 more appropriate than 200?

  3. There is an intentional design inconsistency in createBookmark. What is it, and how would you fix it?

  4. How do updateBookmark and deleteBookmark handle the case where the target bookmark does not exist?

  5. Look at the endpoints defined across server/index.js and the controllers. For each endpoint, observe the HTTP method, URL pattern, and which CRUD operation it performs. How do these endpoints follow REST conventions?

chevron-rightAnswershashtag
  1. URL parameters are always strings. bookmarkModel.find() compares with ===, so "1" === 1 would be false. Number(id) converts the string to a number so the comparison works correctly.

  2. 201 Created is more semantically accurate — it signals that a new resource was successfully created, not just that the request succeeded. 200 OK typically means the request succeeded but no new resource was created.

  3. createBookmark adds newBookmark.createdAt = new Date().toISOString() in the controller. Adding fields to the data is a Model responsibility, not a Controller responsibility. To fix it, move the createdAt assignment into bookmarkModel.create().

  4. updateBookmark and deleteBookmark both send 404 responses if the associated bookmarkModel method fails (if bookmarkModel.update() returns null and bookmarkModel.destroy() returns false). Both use return to short-circuit so res.send() is not called a second time.

  5. REST analysis:

Method
URL
CRUD
Notes

GET

/api/bookmarks

Read

Returns the full collection

GET

/api/bookmarks/:id

Read

Returns a single resource by ID

POST

/api/bookmarks

Create

Creates a new resource in the collection

PATCH

/api/bookmarks/:id

Update

Partially updates a single resource by ID

DELETE

/api/bookmarks/:id

Delete

Removes a single resource by ID

These follow REST conventions: resources are identified by URL (/api/bookmarks and /api/bookmarks/:id), HTTP methods express intent, and the response codes reflect the outcome.

frontend/src/fetch-helpers.js

  1. The mod-4 fetch helpers targeted external URLs like https://dummyjson.com/recipes. These target /api/bookmarks. What is the difference, and why does this work?

  2. All three helpers use async/await with try/catch. What does each helper return on failure?

  3. createBookmark includes a headers object and a body. Why are both needed when making a POST request? What other requests include headers? Why does a DELETE request NOT need headers?

chevron-rightAnswershashtag
  1. /api/bookmarks is a relative URL — it automatically prepends the current origin (http://localhost:8080). It works because the frontend is served by the same Express server as the API, so both share the same origin.

  2. getBookmarks returns [] on failure. createBookmark returns null on failure. deleteBookmark returns false on failure. Callers must check for these values before using the result.

  3. headers: { 'Content-Type': 'application/json' } tells the server the body is JSON-formatted text. body: JSON.stringify(...) converts the JavaScript object into a JSON string. Both are required — without the header, express.json() won't parse the body; without JSON.stringify, the body would be sent as [object Object]. PATCH requests also need headers since they also include a request body. DELETE requests have no request body so they don't need headers.

Concepts Checklist

Backend/Server Application

Frontend/Client Application:

Last updated