Case Study: Bookmark Manager
Follow along with code examples here!
Setup
cd server
npm i
npm run devThe server will be running at http://localhost:8080.
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.

The completed solution files are:
server/index.js— Express server with middleware and routesserver/models/bookmarkModel.js— In-memory data modelserver/controllers/bookmarkControllers.js— Route handler functionsfrontend/index.html— The HTML structurefrontend/src/fetch-helpers.js— Functions that call the APIfrontend/src/dom-helpers.js— Functions that update the DOMfrontend/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
frontend/src/main.js:main()is called on page load.frontend/src/main.js:await getBookmarks()is called.frontend/src/fetch-helpers.js:getBookmarks()sends aGET /api/bookmarksrequest to the server.server/index.js: ThelogRoutesmiddleware logs the request, thenapp.get('/api/bookmarks', listBookmarks)matches and callslistBookmarks.server/controllers/bookmarkControllers.js:listBookmarks()callsbookmarkModel.list().server/models/bookmarkModel.js:bookmarkModel.list()returns a shallow copy of the in-memory bookmarks array.server/controllers/bookmarkControllers.js:res.send(bookmarks)sends the array as JSON to the client.frontend/src/fetch-helpers.js:response.json()parses the JSON and the array is returned.frontend/src/main.js:renderBookmarks(bookmarks)is called with the resolved array.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
Answer
frontend/src/main.js: Thesubmitevent fires on#bookmark-form, callinghandleFormSubmit.frontend/src/main.js: Thetitleandurlare read fromform.title.valueandform.url.valueandawait createBookmark(title, url)is called.frontend/src/fetch-helpers.js:createBookmark()sends aPOST /api/bookmarksrequest withContent-Type: application/jsonand the bookmark data stringified in the request body.server/index.js:express.json()middleware parses the request body intoreq.body.app.post('/api/bookmarks', createBookmark)matches and calls thecreateBookmarkcontroller.server/controllers/bookmarkControllers.js: The controller validatestitleandurl, callsbookmarkModel.create(title, url).server/models/bookmarkModel.js:bookmarkModel.create()generates a newid, pushes the new bookmark into the array, and returns it.server/controllers/bookmarkControllers.js: The controller adds acreatedAttimestamp to the new bookmark (which should happen in the model) and and sends a201response with the new bookmark.frontend/src/main.js:await getBookmarks()is called to retrieve the full updated list.frontend/src/main.js:renderBookmarks(updated)re-renders all bookmarks.
Scenario 3: The user clicks the Delete button on a bookmark
Answer
frontend/src/main.js: A click event fires on#bookmarks-list, callinghandleDeleteBookmarkClick.frontend/src/main.js:handleDeleteBookmarkClickgets the bookmark'sidfrom the clicked button'sdata-bookmark-idattribute and callsawait deleteBookmark(clickedBtn.dataset.bookmarkId).frontend/src/fetch-helpers.js:deleteBookmark()sends aDELETE /api/bookmarks/:idrequest to the server.server/index.js:app.delete('/api/bookmarks/:id', deleteBookmark)matches and calls thedeleteBookmarkcontroller.server/controllers/bookmarkControllers.js: The controller callsbookmarkModel.destroy(Number(id)).server/models/bookmarkModel.js:bookmarkModel.destroy()finds the bookmark by index, removes it withsplice, and returnstrue. Returnsfalseif not found.server/controllers/bookmarkControllers.js:res.sendStatus(204)sends an empty204 No Contentresponse.frontend/src/main.js:await getBookmarks()re-fetches the updated list, thenrenderBookmarks(updated)re-renders.
Guided Reading Questions
Open each file and answer the questions.
server/index.js
server/index.jsWhat does
express.json()middleware do, and why is it needed?What does
express.static(pathToFrontend)do? What is the__dirnamevariable's value and how is it used to construct the finalpathToFrontendvalue? (console log both__dirnameandpathToFrontendto find out)What does the
logRoutesmiddleware do, and what happens if you remove thenext()call?With the serving running, use
curlto test each endpoint below. For each, record the status code, the terminal log printed by the server, and a brief description of the response:
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!
Answers
express.json()parses incoming requests with a JSON body and attaches the result toreq.body. Without it,req.bodywould beundefinedwhen a client sends JSON (e.g., onPOSTorPATCHrequests).express.static()serves all files in a given folder as static assets.__dirnameis a Node.js variable that holds the absolute path to the directory of the current file — in this case, theswe-casestudy-5/server/folder.path.join(__dirname, '../frontend')navigates one level up and intofrontend/to create the absolute path to thefrontend/folder which is stored inpathToFrontend. Visitinghttp://localhost:8080then deliversfrontend/index.htmlautomatically.logRouteslogs the HTTP method, URL, and timestamp for every incoming request. Ifnext()is removed the middleware would never pass control to the next handler in the chain and sincelogRoutesdoesn't send a response itself, the request would "hang" (the client would be waiting forever).Sample curl commands and responses:
server/models/bookmarkModel.js
server/models/bookmarkModel.jsWhere is the "database" stored? What are its limitations compared to a real database? What happens to the bookmark data if you restart the server?
Why does
bookmarkModel.list()return[...bookmarks]instead of justbookmarks? Why dofindandupdatereturn{ ...bookmark }instead ofbookmark? How does this relate to separation of concerns?What does
bookmarkModel.update()return if no bookmark matches theid? What doesbookmarkModel.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?
Answers
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.[...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 thebookmarksdata and creates clearer separation of concerns.bookmarkModel.update()returnsnullexplicitly when no bookmark matches.bookmarkModel.destroy()returnsfalsewhen 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 andnullis the absence of a valid object. MeanwhilebookmarkModel.destroy()returnstrueandfalseis the opposite/absence of atruevalue.
server/controllers/bookmarkControllers.js
server/controllers/bookmarkControllers.jsWhy does
getBookmarkcallNumber(id)whenidcomes fromreq.params?What HTTP status code does
createBookmarksend on success, and why is201more appropriate than200?There is an intentional design inconsistency in
createBookmark. What is it, and how would you fix it?How do
updateBookmarkanddeleteBookmarkhandle the case where the target bookmark does not exist?Look at the endpoints defined across
server/index.jsand the controllers. For each endpoint, observe the HTTP method, URL pattern, and which CRUD operation it performs. How do these endpoints follow REST conventions?
Answers
URL parameters are always strings.
bookmarkModel.find()compares with===, so"1" === 1would befalse.Number(id)converts the string to a number so the comparison works correctly.201 Createdis more semantically accurate — it signals that a new resource was successfully created, not just that the request succeeded.200 OKtypically means the request succeeded but no new resource was created.createBookmarkaddsnewBookmark.createdAt = new Date().toISOString()in the controller. Adding fields to the data is a Model responsibility, not a Controller responsibility. To fix it, move thecreatedAtassignment intobookmarkModel.create().updateBookmarkanddeleteBookmarkboth send404responses if the associatedbookmarkModelmethod fails (ifbookmarkModel.update()returnsnullandbookmarkModel.destroy()returnsfalse). Both usereturnto short-circuit sores.send()is not called a second time.REST analysis:
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
frontend/src/fetch-helpers.jsThe 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?All three helpers use
async/awaitwithtry/catch. What does each helper return on failure?createBookmarkincludes aheadersobject and abody. Why are both needed when making aPOSTrequest? What other requests includeheaders? Why does aDELETErequest NOT needheaders?
Answers
/api/bookmarksis 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.getBookmarksreturns[]on failure.createBookmarkreturnsnullon failure.deleteBookmarkreturnsfalseon failure. Callers must check for these values before using the result.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; withoutJSON.stringify, the body would be sent as[object Object].PATCHrequests also needheaderssince they also include a request body.DELETErequests have no request body so they don't needheaders.
Concepts Checklist
Backend/Server Application
Frontend/Client Application:
Last updated