Our introductory Claude Code tutorial covered installation, configuration, and the philosophy behind “vibe coding.” Now it’s time to build something real. This hands-on tutorial walks through creating a complete Bookmark Manager web application—from empty directory to working app—using nothing but natural language prompts. By the end, you’ll understand the “Explore, Plan, Code, Commit” workflow that Anthropic recommends for production-quality results.
We’re building a Bookmark Manager because it’s the Goldilocks project for demonstrating Claude Code: complex enough to require multiple files, database integration, and a real UI—but simple enough to complete in one session. You’ll create, read, update, and delete bookmarks, organize them by category, and import existing bookmarks from a URL.
Prerequisites
Before starting, ensure you have:
- Claude Code installed –
npm install -g @anthropic-ai/claude-code - Node.js 18+ – Required for Claude Code and our project
- A Claude subscription – Pro ($20/month) or Max ($100/month)
- An empty project directory – We’ll build everything from scratch
Verify your installation by running claude --version in your terminal. You should see version 1.0.30 or higher.
Step 1: Project setup with CLAUDE.md
Create a new directory and initialize the project. The CLAUDE.md file is your project’s instruction manual—it tells Claude your preferences before every conversation.
mkdir bookmark-manager
cd bookmark-manager
git init
Now create a CLAUDE.md file with project-specific instructions. This is the most underutilized feature in Claude Code—a well-crafted CLAUDE.md eliminates repetitive prompting and ensures consistency:
# Bookmark Manager Project
## Tech Stack
- **Frontend:** React 19 with TypeScript
- **Backend:** Express.js with TypeScript
- **Database:** SQLite (via better-sqlite3)
- **Styling:** Tailwind CSS
- **Build:** Vite
## Project Structure
```
/src
/client # React frontend
/server # Express backend
/shared # Shared types
```
## Coding Conventions
- Use TypeScript strict mode
- Prefer async/await over callbacks
- Use functional React components with hooks
- API routes should be RESTful
- All database operations in /src/server/db.ts
## Commands
- `npm run dev` - Start both frontend and backend
- `npm run build` - Build for production
- `npm run test` - Run tests
## Important
- SQLite database file: ./data/bookmarks.db
- Never commit the database file
- Use prepared statements for all queries
This tells Claude exactly what you expect. When you ask it to “add a new feature,” it knows to use React with TypeScript, put database code in db.ts, and follow your preferred patterns.
Step 2: Initial scaffold with “Explore, Plan, Code”
Launch Claude Code and use the workflow Anthropic engineers follow. The key insight: prevent Claude from coding immediately. Ask it to plan first.
claude
Your first prompt should establish the full scope:
> I want to build a Bookmark Manager web app. Before writing any code,
think through the complete architecture. What files do we need? What's
the database schema? What API endpoints? Give me a comprehensive plan
I can review before we start coding.
Claude will produce a detailed plan. Review it carefully. The “think” keyword triggers Claude’s extended thinking mode, giving it more computation time to evaluate alternatives. For even more thorough planning, use “think hard” or “ultrathink.”
Once you approve the plan, scaffold the project:
> Perfect. Now create the complete project scaffold with all the files
from your plan. Initialize npm, install dependencies, and set up the
directory structure. Don't implement logic yet—just create the file
structure with placeholder exports.
Claude will create 15-20 files across your project: package.json, TypeScript configs, Vite config, React components, Express routes, database schema, and shared types. Watch the terminal as it works—you’ll see it editing files, running commands, and committing changes.
Step 3: Database layer
With the scaffold in place, implement the database. Claude Code’s strength is understanding context across files—it knows your schema should match your TypeScript types.
> Implement the database layer in src/server/db.ts. Create the SQLite
database, initialize the schema for bookmarks (id, title, url,
description, category, favicon_url, created_at), and write CRUD
functions. Use prepared statements and make it type-safe.
The resulting code should look something like this (Claude generates this, you don’t type it):
// src/server/db.ts
import Database from 'better-sqlite3';
import path from 'path';
import { Bookmark, CreateBookmarkInput } from '../shared/types';
const dbPath = path.join(process.cwd(), 'data', 'bookmarks.db');
const db = new Database(dbPath);
// Initialize schema
db.exec(`
CREATE TABLE IF NOT EXISTS bookmarks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
url TEXT NOT NULL UNIQUE,
description TEXT,
category TEXT DEFAULT 'Uncategorized',
favicon_url TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_category ON bookmarks(category);
`);
export function getAllBookmarks(): Bookmark[] {
return db.prepare('SELECT * FROM bookmarks ORDER BY created_at DESC').all() as Bookmark[];
}
export function getBookmarkById(id: number): Bookmark | undefined {
return db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id) as Bookmark | undefined;
}
export function createBookmark(input: CreateBookmarkInput): Bookmark {
const stmt = db.prepare(`
INSERT INTO bookmarks (title, url, description, category, favicon_url)
VALUES (@title, @url, @description, @category, @favicon_url)
`);
const result = stmt.run(input);
return getBookmarkById(Number(result.lastInsertRowid))!;
}
export function updateBookmark(id: number, input: Partial<CreateBookmarkInput>): Bookmark | undefined {
const fields = Object.keys(input).map(k => `${k} = @${k}`).join(', ');
const stmt = db.prepare(`UPDATE bookmarks SET ${fields} WHERE id = @id`);
stmt.run({ ...input, id });
return getBookmarkById(id);
}
export function deleteBookmark(id: number): boolean {
const result = db.prepare('DELETE FROM bookmarks WHERE id = ?').run(id);
return result.changes > 0;
}
export function getCategories(): string[] {
const rows = db.prepare('SELECT DISTINCT category FROM bookmarks ORDER BY category').all() as { category: string }[];
return rows.map(r => r.category);
}
Notice how Claude uses prepared statements (as specified in CLAUDE.md), returns proper TypeScript types, and creates useful indexes. This is where the planning phase pays off—Claude knows the full context.
Step 4: API routes
Build the Express API that exposes your database operations:
> Create the Express API routes in src/server/routes.ts. Implement
RESTful endpoints for all CRUD operations. Include input validation,
proper error handling, and fetch the favicon from the URL when
creating bookmarks.
Claude will create routes like:
// src/server/routes.ts
import { Router, Request, Response } from 'express';
import * as db from './db';
import { CreateBookmarkInput } from '../shared/types';
const router = Router();
// GET /api/bookmarks
router.get('/bookmarks', (req: Request, res: Response) => {
const bookmarks = db.getAllBookmarks();
res.json(bookmarks);
});
// GET /api/bookmarks/:id
router.get('/bookmarks/:id', (req: Request, res: Response) => {
const bookmark = db.getBookmarkById(Number(req.params.id));
if (!bookmark) {
return res.status(404).json({ error: 'Bookmark not found' });
}
res.json(bookmark);
});
// POST /api/bookmarks
router.post('/bookmarks', async (req: Request, res: Response) => {
try {
const input: CreateBookmarkInput = req.body;
// Validate required fields
if (!input.url || !input.title) {
return res.status(400).json({ error: 'URL and title are required' });
}
// Fetch favicon
const faviconUrl = await fetchFavicon(input.url);
const bookmark = db.createBookmark({ ...input, favicon_url: faviconUrl });
res.status(201).json(bookmark);
} catch (error: any) {
if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
return res.status(409).json({ error: 'Bookmark already exists' });
}
res.status(500).json({ error: error.message });
}
});
// PUT /api/bookmarks/:id
router.put('/bookmarks/:id', (req: Request, res: Response) => {
const bookmark = db.updateBookmark(Number(req.params.id), req.body);
if (!bookmark) {
return res.status(404).json({ error: 'Bookmark not found' });
}
res.json(bookmark);
});
// DELETE /api/bookmarks/:id
router.delete('/bookmarks/:id', (req: Request, res: Response) => {
const deleted = db.deleteBookmark(Number(req.params.id));
if (!deleted) {
return res.status(404).json({ error: 'Bookmark not found' });
}
res.status(204).send();
});
// GET /api/categories
router.get('/categories', (req: Request, res: Response) => {
res.json(db.getCategories());
});
async function fetchFavicon(url: string): Promise<string | null> {
try {
const domain = new URL(url).origin;
return `${domain}/favicon.ico`;
} catch {
return null;
}
}
export default router;
Step 5: React frontend
Now build the UI. Claude Code excels at multi-file React projects because it maintains context across components:
> Build the React frontend. Create components for: BookmarkList (shows
all bookmarks in a grid), BookmarkCard (individual bookmark with
favicon, title, URL), AddBookmarkForm (modal form for new bookmarks),
CategorySidebar (filter by category). Use Tailwind for styling. Make
it look modern and clean.
Claude will create multiple component files. Here’s what the main BookmarkCard might look like:
// src/client/components/BookmarkCard.tsx
import { Bookmark } from '../../shared/types';
import { Trash2, ExternalLink, Edit2 } from 'lucide-react';
interface Props {
bookmark: Bookmark;
onDelete: (id: number) => void;
onEdit: (bookmark: Bookmark) => void;
}
export function BookmarkCard({ bookmark, onDelete, onEdit }: Props) {
return (
<div className="group relative bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow p-4 border border-gray-100">
<div className="flex items-start gap-3">
<img
src={bookmark.favicon_url || '/default-favicon.png'}
alt=""
className="w-8 h-8 rounded"
onError={(e) => (e.currentTarget.src = '/default-favicon.png')}
/>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 truncate">{bookmark.title}</h3>
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:underline truncate block"
>
{bookmark.url}
</a>
{bookmark.description && (
<p className="text-sm text-gray-500 mt-1 line-clamp-2">{bookmark.description}</p>
)}
<span className="inline-block mt-2 px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded">
{bookmark.category}
</span>
</div>
</div>
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1">
<button onClick={() => onEdit(bookmark)} className="p-1 hover:bg-gray-100 rounded">
<Edit2 size={16} className="text-gray-400" />
</button>
<button onClick={() => onDelete(bookmark.id)} className="p-1 hover:bg-red-50 rounded">
<Trash2 size={16} className="text-red-400" />
</button>
<a href={bookmark.url} target="_blank" rel="noopener" className="p-1 hover:bg-gray-100 rounded">
<ExternalLink size={16} className="text-gray-400" />
</a>
</div>
</div>
);
}

Step 6: Wiring it together
Connect the frontend to the backend:
> Create an API client in src/client/api.ts that wraps fetch calls to
our backend. Then update the App component to fetch bookmarks on
mount, handle CRUD operations, and manage state. Add loading and
error states.
Test the application:
> Run the dev server and make sure everything works. If there are any
errors, fix them.
Claude will run npm run dev, watch the output, and automatically fix any TypeScript errors, missing imports, or configuration issues. This is where the agentic nature shines—it doesn’t just show you errors, it fixes them.
Step 7: Adding a power feature
Let’s add URL import—paste any URL and automatically extract the page title:
> Add a feature where users can paste a URL and we automatically fetch
the page title. Create a new API endpoint POST /api/bookmarks/import
that takes just a URL, fetches the page, extracts the title and
description from meta tags, and creates the bookmark. Update the
frontend to support this.
Claude will add server-side HTML parsing, create the new endpoint, and update the UI with a “Quick Add” button. Watch as it coordinates changes across multiple files—this is impossible with traditional autocomplete.
Step 8: Testing with TDD approach
Anthropic recommends test-driven development for verifiable changes. Add tests to ensure the API works correctly:
> Write integration tests for the API using Vitest. Test all CRUD
operations and edge cases. Use a test database. Run the tests and
fix any failures.
Claude creates test files, configures Vitest, runs the tests, and iterates on failures until everything passes. The “run the tests and fix any failures” instruction creates a feedback loop that’s powerful for quality.
Step 9: Committing your work
Claude Code integrates with git. Commit your progress:
> Review all the changes we've made with git diff, then create a
meaningful commit with a conventional commit message.
Claude will show you the diff, summarize the changes, and create a commit like: feat: implement complete bookmark manager with CRUD, categories, and URL import.
Pro tips for production workflows
Now that you’ve built a complete app, here are advanced techniques for real projects:
Use checkpoints. Before major changes, Claude Code now saves your project state automatically. Use /rewind to roll back if something goes wrong. You can also tap Esc twice to revert the last change.
Create custom commands. Store frequently-used prompts in .claude/commands/. For example, .claude/commands/review.md:
Review the current changes for:
1. TypeScript type safety issues
2. Security vulnerabilities
3. Performance concerns
4. Missing error handling
Suggest improvements but don't make changes yet.
Then use it with /review in Claude Code.
Clear context regularly. Use /clear between distinct tasks to prevent context contamination. Each conversation should focus on one feature or fix.
Leverage MCP servers. Claude Code can connect to external tools through the Model Context Protocol. Configure database inspectors, API clients, or documentation fetchers in .mcp.json to give Claude real-time access to external systems.
What you built
In one session, you created:
- A full-stack TypeScript application with React and Express
- SQLite database with proper schema and indexes
- RESTful API with validation and error handling
- Modern React UI with Tailwind CSS
- URL import with automatic metadata extraction
- Integration tests with Vitest
More importantly, you learned the workflow that makes Claude Code effective: plan before coding, provide context through CLAUDE.md, iterate in small steps, and use the testing feedback loop to catch issues early.
The complete project structure is available for reference. Run /tree in Claude Code to see everything Claude generated, or clone the Claude Code repository for more examples of what’s possible with agentic coding.
Get the Daily Pulse
Sharp analysis on what's actually moving in AI. No hype, no filler, no weekly digest.



