A full-stack bookmark manager built as a Frontend Mentor challenge — save, organize, search, filter, and sort your bookmarks behind real user accounts, not just a static mock of one.
Users can:
- Add new bookmarks with a title, description, website URL, and tags
- View all their bookmarks, with favicon, title, URL, description, tags, view count, last visited date, and date added
- Search for bookmarks by title
- Filter bookmarks by one or multiple tags from the sidebar, and reset filters
- View and manage archived bookmarks separately from the main view
- Archive/unarchive bookmarks without deleting them
- Pin/unpin bookmarks to keep important ones easily accessible
- Edit existing bookmarks
- Copy bookmark URLs to the clipboard
- Visit bookmarked websites directly from the app
- Sort bookmarks by "Recently added", "Recently visited", or "Most visited"
- Toggle between light and dark color themes
- Sign up, log in, log out, and reset a forgotten password by email
- Use the app at the optimal layout for their device, with visible hover/focus states throughout
Desktop (1440px)
Mobile (375px)
- Solution URL: frontendmentor.io/solutions/full-stack-bookmark-manager-using-react-19-ts-vite-express-and-prisma-bBYH4fWQt8
- Live Site URL: fsdev-bookmark-manager-frontend.vercel.app
This was built as a full-stack app rather than a static frontend reading data.json — real accounts, real JWT auth, and a real Postgres database — as a deliberate step up from the challenge's baseline scope.
apps/
├── frontend/ React 19 + TypeScript + Vite, CSS Modules, React Router
└── backend/ Express + TypeScript + Prisma (Postgres), JWT auth, Resend email
- Auth: signup/login issue a JWT in an httpOnly cookie; bcrypt hashes passwords; forgot/reset password generates a time-limited token and emails a reset link via Resend.
- Data seeding: on first signup, each user's account is seeded with the bookmarks from
data.json, so every account starts populated — matching the challenge's "populate content on first load" behavior, just per-user instead of shared/static. - Favicons: seeded bookmarks reuse the provided local favicon assets; bookmarks added afterward get their favicon fetched automatically from the site's domain.
apps/backend/src/
├── routes/ auth.ts (signup/login/logout/me/forgot/reset), bookmarks.ts (CRUD, search, sort, tags)
├── middleware/ JWT auth guard
├── lib/ prisma client, jwt signing, favicon lookup, Resend email, data.json seeding
└── prisma/ schema.prisma + migrations
apps/frontend/src/
├── pages/ SignIn/SignUp/ForgotPassword/ResetPassword, HomePage (active + archived views)
├── components/ Header, Sidebar, BookmarkCard/Grid, AddEditBookmarkModal, ConfirmDialog, ActionsMenu, ProfileMenu
├── context/ AuthContext, ThemeContext, BookmarksContext
└── styles/ design tokens (variables.css) pulled from the Figma design system
- Semantic HTML5 markup, one
<main>/<h1>per page - CSS Modules with design tokens as CSS custom properties (colors, spacing, radius, typography, shadows)
- Mobile-first responsive layout (375 / 768 / 1440 breakpoints)
- React 19 + React Router
- Vite
- Express + Prisma + PostgreSQL (Neon)
- Resend for transactional email
- Vitest + React Testing Library
- Node.js >= 20
- A PostgreSQL database (local via Docker, or a Neon project)
- A Resend API key (optional in local dev — without it, reset links are logged to the server console instead of emailed)
git clone https://github.com/gusanchefullstack/fsdev-bookmark-manager-app.git
cd fsdev-bookmark-manager-app
npm installCopy the example env files and fill them in:
cp apps/backend/.env.example apps/backend/.env
cp apps/frontend/.env.example apps/frontend/.envapps/backend/.env
| Variable | Description | Required | Default |
|---|---|---|---|
DATABASE_URL |
Postgres connection string | Yes | — |
JWT_SECRET |
Secret used to sign session JWTs | Yes | — |
RESEND_API_KEY |
Resend API key for password-reset emails | No | logs the reset link instead |
FRONTEND_URL |
Frontend origin, for CORS + reset links | No | http://localhost:5173 |
PORT |
API server port | No | 4000 |
apps/frontend/.env
| Variable | Description | Required | Default |
|---|---|---|---|
VITE_API_URL |
Base URL the frontend calls for the API | No | /api |
# apply the Prisma schema to your database
npm run dev -w apps/backend -- --run prisma:migrate # or: cd apps/backend && npx prisma migrate dev
# in one terminal
npm run dev:backend
# in another terminal
npm run dev:frontendThe frontend runs at http://localhost:5173 and proxies /api to the backend at http://localhost:4000.
All /api/bookmarks routes require an authenticated session (the bm_token httpOnly cookie set by signup/login).
{ "name": "Ada Lovelace", "email": "ada@example.com", "password": "supersecret" }Response 201:
{ "user": { "id": "…", "email": "ada@example.com", "name": "Ada Lovelace", "avatarUrl": null } }Response 200:
{ "bookmarks": [{ "id": "…", "title": "React Docs", "url": "https://react.dev", "tags": ["JavaScript", "Framework"], "pinned": false, "visitCount": 12 }] }{ "title": "Vitest", "description": "A blazing fast unit test framework.", "url": "https://vitest.dev", "tags": ["Testing", "Tools"] }Accepts any subset of title, description, url, tags, pinned, isArchived.
Permanently deletes a bookmark, or records a visit (increments visitCount, sets lastVisited).
npm run test:backend # 15 vitest tests: auth + bookmarks routes against an in-memory fake Prisma client
npm run test:frontend # 16 vitest + React Testing Library tests: components, hooks, and utils
npm test # both- Design tokens from Figma, not hardcoded values. Pulling
get_variable_defsfrom the Figma Desktop MCP gave a concrete color/spacing/typography scale to translate into CSS custom properties (apps/frontend/src/styles/variables.css) up front, instead of eyeballing colors from screenshots later. - Seed data has to travel with the deployed backend. The first version of the seeding logic read
data.jsonfrom the repo root via a relative filesystem path — fine locally, but it would have broken the moment the backend ran as a Vercel serverless function with a different working directory. Importing the seed data as a JSON module from insidesrc/instead makes it part of the bundle. - jsdom needs a real origin before
localStorageexists. Vitest's jsdom environment doesn't exposewindow.localStorageunless you give it an explicithttp(s)URL (and even then, Node's own experimental globallocalStoragecan shadow it) — worth a small polyfill in the test setup file rather than a surprise failure per test file.
- Wire up real-time favicon refresh (currently only re-fetched when a bookmark's URL changes)
- Add optimistic UI updates for pin/archive so the card animates before the network round-trip resolves
- Expand the Vercel deployment to attach a custom domain and add uptime monitoring
- Prisma with PostgreSQL guide — used this to get the schema and migration workflow set up correctly against Neon.
- React Router v6 data APIs — clarified how nested
<Routes>inside a route element resolve paths, which is what makes the/vs/archivedsplit inside the authenticated app shell work.
This project was built with Claude Code as a pair-programming assistant:
- Planning: Claude explored the Figma file via the Figma Desktop MCP (design system tokens, screen layouts across breakpoints and states) and the challenge's
data.json/assets before proposing an implementation plan, which was reviewed and approved before any code was written. - Implementation: Claude wrote the monorepo scaffold, backend API, and all frontend components/tests, verifying each responsive breakpoint and CRUD flow directly in a real browser (and against a local Postgres instance) rather than relying on typechecking alone.
- What worked well: catching a real deployment bug during manual verification (the seed-data file path issue above) before it ever reached production, and getting pixel-accurate screenshots by scripting a headless Chrome session instead of relying on visual approximation.
- What didn't: browser-automation tooling for resizing the viewport to exact breakpoint widths was unreliable mid-session; falling back to a small Puppeteer script against a locally installed Chrome binary was more dependable for the final 375px/1440px screenshots.
- Auth (signup/login/logout, forgot/reset password)
- Bookmark CRUD, search, sort, tag filter, archive, pin
- Responsive layout (mobile/tablet/desktop) matching Figma
- Light/dark theme toggle
- Backend + frontend test suites
- Deploy to Vercel with Neon
- Configure Resend for production password-reset emails
- Submit to Frontend Mentor
This project is for personal portfolio and learning purposes as part of a Frontend Mentor challenge.

