Skip to main content

快速原型开发

上周我用Claude Code从零搭了一个全栈Todo应用,前后端+数据库全都有。本来以为要搞两三天,结果大半天就弄完了。

今天把这个过程写下来,给你看看Claude Code到底能快到什么程度。

项目概述

要做什么

一个完整的Todo应用,包含:

  • React前端(现代化界面)
  • Express.js后端API
  • MongoDB数据库
  • JWT用户认证
  • 完整的CRUD功能
  • 可选的云部署

技术栈

  • 前端: React 18 + React Router + Axios
  • 后端: Node.js + Express + Mongoose
  • 数据库: MongoDB
  • 认证: JWT

节省了多少时间

开发方式时间
传统开发16-24小时
Claude Code2-3小时

大概快了8-10倍。

准备工作

需要什么环境

先确保你的机器上有这些:

node --version  # 需要16+
npm --version
mongod --version # 或者用MongoDB Atlas也行

创建项目

mkdir todo-app-fullstack
cd todo-app-fullstack
git init
git branch -M main

启动Claude Code

claude

进入后会看到欢迎界面,就可以开始对话了。

开始构建

第一句话怎么说

直接告诉Claude Code你要什么:

我想做一个全栈Todo应用:
- React前端(Create React App)
- Express后端API
- MongoDB数据库
- JWT用户认证
- 完整的CRUD功能

帮我规划项目结构,创建基础文件。

Claude Code会自动:

  1. 创建前后端目录结构
  2. 生成配置文件
  3. 初始化package.json
  4. 创建基础文件

最终的项目结构

todo-app-fullstack/
├── backend/
│ ├── src/
│ │ ├── models/
│ │ ├── routes/
│ │ ├── middleware/
│ │ └── server.js
│ └── package.json
├── frontend/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ └── services/
│ └── package.json
└── README.md

后端开发

创建Express服务器

先完成后端。创建Express服务器:
- CORS、body-parser等基础配置
- 错误处理中间件
- MongoDB连接
- 端口5000

Claude Code会生成完整的server.js

backend/src/server.js

const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');
require('dotenv').config();

const app = express();

// 中间件配置
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 数据库连接
mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/todoapp', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('✓ MongoDB connected successfully'))
.catch(err => console.error('MongoDB connection error:', err));

// 路由
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', message: 'Server is running' });
});

// 路由导入(稍后添加)
const authRoutes = require('./routes/auth');
const todoRoutes = require('./routes/todos');

app.use('/api/auth', authRoutes);
app.use('/api/todos', todoRoutes);

// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: err.message || 'Something went wrong!',
});
});

// 404处理
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'Route not found',
});
});

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
console.log(`✓ Server is running on port ${PORT}`);
console.log(`✓ API available at http://localhost:${PORT}/api`);
});

module.exports = app;

创建数据模型

创建User和Todo的Mongoose模型:

User模型:username(唯一)、email(唯一)、password(哈希)、创建时间
Todo模型:title、description、completed、priority、user引用、时间戳

backend/src/models/User.js:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Username is required'],
unique: true,
trim: true,
minlength: [3, 'Username must be at least 3 characters'],
maxlength: [30, 'Username cannot exceed 30 characters'],
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
trim: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email'],
},
password: {
type: String,
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters'],
},
createdAt: {
type: Date,
default: Date.now,
},
});

// 密码哈希中间件
userSchema.pre('save', async function(next) {
// 只在密码被修改时才进行哈希
if (!this.isModified('password')) {
return next();
}

try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});

// 密码验证方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};

// 移除密码字段
userSchema.methods.toJSON = function() {
const user = this.toObject();
delete user.password;
return user;
};

module.exports = mongoose.model('User', userSchema);

backend/src/models/Todo.js:

const mongoose = require('mongoose');

const todoSchema = new mongoose.Schema({
title: {
type: String,
required: [true, 'Todo title is required'],
trim: true,
maxlength: [100, 'Title cannot exceed 100 characters'],
},
description: {
type: String,
trim: true,
maxlength: [500, 'Description cannot exceed 500 characters'],
},
completed: {
type: Boolean,
default: false,
},
priority: {
type: String,
enum: ['low', 'medium', 'high'],
default: 'medium',
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: {
type: Date,
default: Date.now,
},
});

// 更新时间戳
todoSchema.pre('save', function(next) {
this.updatedAt = Date.now();
next();
});

module.exports = mongoose.model('Todo', todoSchema);

实现认证系统

创建认证路由和中间件:
- 注册接口 POST /api/auth/register
- 登录接口 POST /api/auth/login
- JWT认证中间件
- 获取当前用户 GET /api/auth/me

backend/src/middleware/auth.js:

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const auth = async (req, res, next) => {
try {
// 从请求头获取token
const token = req.header('Authorization')?.replace('Bearer ', '');

if (!token) {
return res.status(401).json({
success: false,
message: 'No authentication token, access denied',
});
}

// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key');

// 查找用户
const user = await User.findById(decoded.userId).select('-password');

if (!user) {
return res.status(401).json({
success: false,
message: 'User not found',
});
}

// 将用户信息添加到请求对象
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Token is not valid',
});
}
};

module.exports = auth;

backend/src/routes/auth.js:

const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = require('../middleware/auth');

// 生成JWT Token
const generateToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '7d' }
);
};

// 注册
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;

// 验证必填字段
if (!username || !email || !password) {
return res.status(400).json({
success: false,
message: 'Please provide all required fields',
});
}

// 检查用户是否已存在
const existingUser = await User.findOne({
$or: [{ email }, { username }],
});

if (existingUser) {
return res.status(400).json({
success: false,
message: 'User with this email or username already exists',
});
}

// 创建新用户
const user = new User({ username, email, password });
await user.save();

// 生成token
const token = generateToken(user._id);

res.status(201).json({
success: true,
message: 'User registered successfully',
data: {
user: user.toJSON(),
token,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

// 登录
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;

// 验证必填字段
if (!email || !password) {
return res.status(400).json({
success: false,
message: 'Please provide email and password',
});
}

// 查找用户
const user = await User.findOne({ email });

if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}

// 验证密码
const isPasswordValid = await user.comparePassword(password);

if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: 'Invalid credentials',
});
}

// 生成token
const token = generateToken(user._id);

res.json({
success: true,
message: 'Login successful',
data: {
user: user.toJSON(),
token,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

// 获取当前用户信息
router.get('/me', auth, async (req, res) => {
res.json({
success: true,
data: {
user: req.user,
},
});
});

module.exports = router;

实现Todo CRUD API

创建Todo的CRUD路由:
- GET /api/todos - 获取所有
- POST /api/todos - 创建
- PUT /api/todos/:id - 更新
- DELETE /api/todos/:id - 删除
- PATCH /api/todos/:id/toggle - 切换状态

所有路由需要认证。

backend/src/routes/todos.js:

const express = require('express');
const router = express.Router();
const Todo = require('../models/Todo');
const auth = require('../middleware/auth');

// 所有路由都需要认证
router.use(auth);

// 获取当前用户的所有todos
router.get('/', async (req, res) => {
try {
const { completed, priority, sort } = req.query;

// 构建查询条件
const query = { user: req.user._id };

if (completed !== undefined) {
query.completed = completed === 'true';
}

if (priority) {
query.priority = priority;
}

// 构建排序
let sortOption = { createdAt: -1 }; // 默认按创建时间降序
if (sort === 'priority') {
sortOption = { priority: 1, createdAt: -1 };
} else if (sort === 'title') {
sortOption = { title: 1 };
}

const todos = await Todo.find(query).sort(sortOption);

res.json({
success: true,
data: {
todos,
count: todos.length,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

// 创建新todo
router.post('/', async (req, res) => {
try {
const { title, description, priority } = req.body;

if (!title) {
return res.status(400).json({
success: false,
message: 'Todo title is required',
});
}

const todo = new Todo({
title,
description,
priority: priority || 'medium',
user: req.user._id,
});

await todo.save();

res.status(201).json({
success: true,
message: 'Todo created successfully',
data: {
todo,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

// 更新todo
router.put('/:id', async (req, res) => {
try {
const { title, description, priority, completed } = req.body;

const todo = await Todo.findOne({
_id: req.params.id,
user: req.user._id,
});

if (!todo) {
return res.status(404).json({
success: false,
message: 'Todo not found',
});
}

// 更新字段
if (title !== undefined) todo.title = title;
if (description !== undefined) todo.description = description;
if (priority !== undefined) todo.priority = priority;
if (completed !== undefined) todo.completed = completed;

await todo.save();

res.json({
success: true,
message: 'Todo updated successfully',
data: {
todo,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

// 切换完成状态
router.patch('/:id/toggle', async (req, res) => {
try {
const todo = await Todo.findOne({
_id: req.params.id,
user: req.user._id,
});

if (!todo) {
return res.status(404).json({
success: false,
message: 'Todo not found',
});
}

todo.completed = !todo.completed;
await todo.save();

res.json({
success: true,
message: 'Todo status toggled successfully',
data: {
todo,
},
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

// 删除todo
router.delete('/:id', async (req, res) => {
try {
const todo = await Todo.findOneAndDelete({
_id: req.params.id,
user: req.user._id,
});

if (!todo) {
return res.status(404).json({
success: false,
message: 'Todo not found',
});
}

res.json({
success: true,
message: 'Todo deleted successfully',
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});

module.exports = router;

环境变量配置

backend/.env.example

PORT=5000
NODE_ENV=development
MONGODB_URI=mongodb://localhost:27017/todoapp
JWT_SECRET=your-secret-key-change-this-in-production
CLIENT_URL=http://localhost:3000

安装依赖

backend/package.json

{
"name": "todo-app-backend",
"version": "1.0.0",
"description": "Backend API for Todo App",
"main": "src/server.js",
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "jest"
},
"keywords": ["todo", "express", "mongodb", "jwt"],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.6.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.7.0"
}
}

安装依赖:

cd backend && npm install

测试后端

npm run dev

看到这个就说明成功了:

✓ Server is running on port 5000
✓ API available at http://localhost:5000/api
✓ MongoDB connected successfully

前端开发

创建React应用

现在做React前端,用Create React App:
- 页面:Login、Register、Dashboard
- 组件:TodoList、TodoItem、TodoForm、Header
- 服务:api.js(Axios)、auth.js
- 上下文:AuthContext

API服务配置

frontend/src/services/api.js:

import axios from 'axios';

const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';

// 创建axios实例
const api = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});

// 请求拦截器 - 添加token
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

// 响应拦截器 - 处理错误
api.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// Token过期或无效,清除本地存储并跳转登录
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
}
return Promise.reject(error.response?.data || error.message);
}
);

export default api;

frontend/src/services/auth.js:

import api from './api';

export const authService = {
// 注册
register: async (userData) => {
const response = await api.post('/auth/register', userData);
if (response.success && response.data.token) {
localStorage.setItem('token', response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
}
return response;
},

// 登录
login: async (credentials) => {
const response = await api.post('/auth/login', credentials);
if (response.success && response.data.token) {
localStorage.setItem('token', response.data.token);
localStorage.setItem('user', JSON.stringify(response.data.user));
}
return response;
},

// 登出
logout: () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
},

// 获取当前用户
getCurrentUser: async () => {
return await api.get('/auth/me');
},

// 检查是否已登录
isAuthenticated: () => {
return !!localStorage.getItem('token');
},

// 获取本地用户信息
getLocalUser: () => {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
},
};

frontend/src/services/todo.js:

import api from './api';

export const todoService = {
// 获取所有todos
getTodos: async (filters = {}) => {
const params = new URLSearchParams(filters).toString();
return await api.get(`/todos?${params}`);
},

// 创建todo
createTodo: async (todoData) => {
return await api.post('/todos', todoData);
},

// 更新todo
updateTodo: async (id, todoData) => {
return await api.put(`/todos/${id}`, todoData);
},

// 切换完成状态
toggleTodo: async (id) => {
return await api.patch(`/todos/${id}/toggle`);
},

// 删除todo
deleteTodo: async (id) => {
return await api.delete(`/todos/${id}`);
},
};

认证上下文

frontend/src/context/AuthContext.js

import React, { createContext, useState, useContext, useEffect } from 'react';
import { authService } from '../services/auth';

const AuthContext = createContext();

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
// 初始化时检查本地存储的用户信息
const localUser = authService.getLocalUser();
if (localUser) {
setUser(localUser);
}
setLoading(false);
}, []);

const login = async (credentials) => {
try {
const response = await authService.login(credentials);
setUser(response.data.user);
return response;
} catch (error) {
throw error;
}
};

const register = async (userData) => {
try {
const response = await authService.register(userData);
setUser(response.data.user);
return response;
} catch (error) {
throw error;
}
};

const logout = () => {
authService.logout();
setUser(null);
};

const value = {
user,
login,
register,
logout,
isAuthenticated: !!user,
loading,
};

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

认证页面

frontend/src/pages/Login.js

import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import './Auth.css';

const Login = () => {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);

const navigate = useNavigate();
const { login } = useAuth();

const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};

const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);

try {
await login(formData);
navigate('/dashboard');
} catch (err) {
setError(err.message || '登录失败,请检查您的凭据');
} finally {
setLoading(false);
}
};

return (
<div className="auth-container">
<div className="auth-card">
<h2>登录</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">邮箱</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
placeholder="输入您的邮箱"
/>
</div>
<div className="form-group">
<label htmlFor="password">密码</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
placeholder="输入您的密码"
/>
</div>
<button type="submit" className="btn-primary" disabled={loading}>
{loading ? '登录中...' : '登录'}
</button>
</form>
<p className="auth-link">
还没有账号? <Link to="/register">立即注册</Link>
</p>
</div>
</div>
);
};

export default Login;

frontend/src/pages/Register.js:

import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import './Auth.css';

const Register = () => {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);

const navigate = useNavigate();
const { register } = useAuth();

const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};

const handleSubmit = async (e) => {
e.preventDefault();
setError('');

// 验证密码
if (formData.password !== formData.confirmPassword) {
setError('两次密码输入不一致');
return;
}

if (formData.password.length < 6) {
setError('密码长度至少为6个字符');
return;
}

setLoading(true);

try {
await register({
username: formData.username,
email: formData.email,
password: formData.password,
});
navigate('/dashboard');
} catch (err) {
setError(err.message || '注册失败,请重试');
} finally {
setLoading(false);
}
};

return (
<div className="auth-container">
<div className="auth-card">
<h2>注册</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">用户名</label>
<input
type="text"
id="username"
name="username"
value={formData.username}
onChange={handleChange}
required
minLength="3"
placeholder="输入用户名"
/>
</div>
<div className="form-group">
<label htmlFor="email">邮箱</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
placeholder="输入邮箱地址"
/>
</div>
<div className="form-group">
<label htmlFor="password">密码</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleChange}
required
minLength="6"
placeholder="输入密码(至少6位)"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">确认密码</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
placeholder="再次输入密码"
/>
</div>
<button type="submit" className="btn-primary" disabled={loading}>
{loading ? '注册中...' : '注册'}
</button>
</form>
<p className="auth-link">
已有账号? <Link to="/login">立即登录</Link>
</p>
</div>
</div>
);
};

export default Register;

Todo组件

frontend/src/components/TodoForm.js

import React, { useState } from 'react';
import './TodoForm.css';

const TodoForm = ({ onSubmit, initialData = null }) => {
const [formData, setFormData] = useState(
initialData || {
title: '',
description: '',
priority: 'medium',
}
);

const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};

const handleSubmit = (e) => {
e.preventDefault();
if (formData.title.trim()) {
onSubmit(formData);
if (!initialData) {
setFormData({
title: '',
description: '',
priority: 'medium',
});
}
}
};

return (
<form className="todo-form" onSubmit={handleSubmit}>
<div className="form-row">
<input
type="text"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="输入任务标题..."
required
className="form-input"
/>
<select
name="priority"
value={formData.priority}
onChange={handleChange}
className="form-select"
>
<option value="low">低优先级</option>
<option value="medium">中优先级</option>
<option value="high">高优先级</option>
</select>
<button type="submit" className="btn-add">
{initialData ? '更新' : '添加'}
</button>
</div>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder="任务描述(可选)..."
className="form-textarea"
rows="2"
/>
</form>
);
};

export default TodoForm;

frontend/src/components/TodoItem.js:

import React, { useState } from 'react';
import TodoForm from './TodoForm';
import './TodoItem.css';

const TodoItem = ({ todo, onToggle, onUpdate, onDelete }) => {
const [isEditing, setIsEditing] = useState(false);

const handleUpdate = (formData) => {
onUpdate(todo._id, formData);
setIsEditing(false);
};

const getPriorityClass = (priority) => {
return `priority-${priority}`;
};

if (isEditing) {
return (
<div className="todo-item editing">
<TodoForm
onSubmit={handleUpdate}
initialData={{
title: todo.title,
description: todo.description,
priority: todo.priority,
}}
/>
<button
onClick={() => setIsEditing(false)}
className="btn-cancel"
>
取消
</button>
</div>
);
}

return (
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<div className="todo-content">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo._id)}
className="todo-checkbox"
/>
<div className="todo-details">
<h3 className="todo-title">{todo.title}</h3>
{todo.description && (
<p className="todo-description">{todo.description}</p>
)}
<div className="todo-meta">
<span className={`todo-priority ${getPriorityClass(todo.priority)}`}>
{todo.priority === 'high' && '高优先级'}
{todo.priority === 'medium' && '中优先级'}
{todo.priority === 'low' && '低优先级'}
</span>
<span className="todo-date">
{new Date(todo.createdAt).toLocaleDateString('zh-CN')}
</span>
</div>
</div>
</div>
<div className="todo-actions">
<button
onClick={() => setIsEditing(true)}
className="btn-edit"
title="编辑"
>
✏️
</button>
<button
onClick={() => onDelete(todo._id)}
className="btn-delete"
title="删除"
>
🗑️
</button>
</div>
</div>
);
};

export default TodoItem;

frontend/src/components/TodoList.js:

import React from 'react';
import TodoItem from './TodoItem';
import './TodoList.css';

const TodoList = ({ todos, onToggle, onUpdate, onDelete }) => {
if (todos.length === 0) {
return (
<div className="empty-state">
<p>暂无任务,开始添加你的第一个任务吧!</p>
</div>
);
}

return (
<div className="todo-list">
{todos.map((todo) => (
<TodoItem
key={todo._id}
todo={todo}
onToggle={onToggle}
onUpdate={onUpdate}
onDelete={onDelete}
/>
))}
</div>
);
};

export default TodoList;

Dashboard页面

frontend/src/pages/Dashboard.js

import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import { todoService } from '../services/todo';
import TodoForm from '../components/TodoForm';
import TodoList from '../components/TodoList';
import Header from '../components/Header';
import './Dashboard.css';

const Dashboard = () => {
const { user } = useAuth();
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [filter, setFilter] = useState('all');

useEffect(() => {
fetchTodos();
}, [filter]);

const fetchTodos = async () => {
try {
setLoading(true);
const filters = {};
if (filter === 'active') {
filters.completed = 'false';
} else if (filter === 'completed') {
filters.completed = 'true';
}
const response = await todoService.getTodos(filters);
setTodos(response.data.todos);
} catch (err) {
setError(err.message || '获取任务列表失败');
} finally {
setLoading(false);
}
};

const handleAddTodo = async (formData) => {
try {
const response = await todoService.createTodo(formData);
setTodos([response.data.todo, ...todos]);
} catch (err) {
setError(err.message || '添加任务失败');
}
};

const handleToggleTodo = async (id) => {
try {
const response = await todoService.toggleTodo(id);
setTodos(
todos.map((todo) =>
todo._id === id ? response.data.todo : todo
)
);
} catch (err) {
setError(err.message || '更新任务失败');
}
};

const handleUpdateTodo = async (id, formData) => {
try {
const response = await todoService.updateTodo(id, formData);
setTodos(
todos.map((todo) =>
todo._id === id ? response.data.todo : todo
)
);
} catch (err) {
setError(err.message || '更新任务失败');
}
};

const handleDeleteTodo = async (id) => {
if (window.confirm('确定要删除这个任务吗?')) {
try {
await todoService.deleteTodo(id);
setTodos(todos.filter((todo) => todo._id !== id));
} catch (err) {
setError(err.message || '删除任务失败');
}
}
};

const stats = {
total: todos.length,
active: todos.filter((t) => !t.completed).length,
completed: todos.filter((t) => t.completed).length,
};

return (
<div className="dashboard">
<Header />
<div className="dashboard-container">
<div className="dashboard-header">
<h1>欢迎, {user?.username}!</h1>
<div className="stats">
<div className="stat-item">
<span className="stat-value">{stats.total}</span>
<span className="stat-label">总任务</span>
</div>
<div className="stat-item">
<span className="stat-value">{stats.active}</span>
<span className="stat-label">进行中</span>
</div>
<div className="stat-item">
<span className="stat-value">{stats.completed}</span>
<span className="stat-label">已完成</span>
</div>
</div>
</div>

{error && <div className="error-message">{error}</div>}

<TodoForm onSubmit={handleAddTodo} />

<div className="filters">
<button
className={filter === 'all' ? 'active' : ''}
onClick={() => setFilter('all')}
>
全部
</button>
<button
className={filter === 'active' ? 'active' : ''}
onClick={() => setFilter('active')}
>
进行中
</button>
<button
className={filter === 'completed' ? 'active' : ''}
onClick={() => setFilter('completed')}
>
已完成
</button>
</div>

{loading ? (
<div className="loading">加载中...</div>
) : (
<TodoList
todos={todos}
onToggle={handleToggleTodo}
onUpdate={handleUpdateTodo}
onDelete={handleDeleteTodo}
/>
)}
</div>
</div>
);
};

export default Dashboard;

样式

frontend/src/index.css

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}

:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--border-color: #e5e7eb;
--bg-white: #ffffff;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

路由配置

frontend/src/App.js

import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import Login from './pages/Login';
import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import './App.css';

// 路由守卫组件
const PrivateRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();

if (loading) {
return <div className="loading-screen">加载中...</div>;
}

return isAuthenticated ? children : <Navigate to="/login" />;
};

const PublicRoute = ({ children }) => {
const { isAuthenticated, loading } = useAuth();

if (loading) {
return <div className="loading-screen">加载中...</div>;
}

return !isAuthenticated ? children : <Navigate to="/dashboard" />;
};

function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Navigate to="/dashboard" />} />
<Route
path="/login"
element={
<PublicRoute>
<Login />
</PublicRoute>
}
/>
<Route
path="/register"
element={
<PublicRoute>
<Register />
</PublicRoute>
}
/>
<Route
path="/dashboard"
element={
<PrivateRoute>
<Dashboard />
</PrivateRoute>
}
/>
</Routes>
);
}

function App() {
return (
<Router>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</Router>
);
}

export default App;

测试

启动应用

# 终端1 - 后端
cd backend && npm run dev

# 终端2 - 前端
cd frontend && npm start

浏览器自动打开 http://localhost:3000

功能测试

注册 → 登录 → 添加任务 → 编辑/删除 → 过滤

有问题直接告诉Claude Code,它会帮你调试。

增强功能

优化用户体验:
- 全局加载动画
- Toast通知
- 移动端适配
添加功能:
- 任务搜索
- 任务排序
- 批量操作
- 截止日期
- 数据导出

Claude Code会逐一实现。

部署

准备部署

准备生产环境:
- 生产配置
- 构建优化
- 环境变量

前端部署

frontend/vercel.json

{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
],
"env": {
"REACT_APP_API_URL": "https://your-api-url.com/api"
}
}

部署步骤:

# 安装Vercel CLI
npm i -g vercel

# 登录并部署
cd frontend
vercel

后端部署

Railway:

  1. 连接GitHub仓库
  2. 选择backend目录
  3. 配置环境变量:
MONGODB_URI=mongodb+srv://...
JWT_SECRET=your-secret
CLIENT_URL=https://your-frontend.vercel.app
NODE_ENV=production

效率对比

开发方式总时间
传统开发17-26小时
Claude Code2-3小时

快了大概8-10倍。

最终效果

功能清单

  • 用户注册登录
  • JWT认证
  • 完整的CRUD
  • 优先级设置
  • 状态过滤
  • 响应式设计
  • 生产部署就绪

使用技巧

清晰描述需求

❌ 不好:

创建一个todo应用

✅ 好:

创建一个todo应用:
- React前端
- Express后端
- MongoDB数据库
- JWT认证
- 完整CRUD

分阶段实现

  1. 搭建项目结构
  2. 后端API
  3. 前端组件
  4. 前后端联调
  5. 优化增强

及时测试

每完成一个阶段就测试,有问题直接告诉Claude Code。

常见问题

Q: 代码有错误怎么办?

直接把错误信息贴给Claude Code:

执行npm start时报错:Error: Cannot find module 'dotenv'

Q: 怎么让Claude Code更好理解项目?

创建CLAUDE.md

# 项目上下文

全栈Todo应用:
- 前端:React 18 + React Router
- 后端:Express + MongoDB
- 认证:JWT

编码规范:
- ES6+语法
- 函数组件
- async/await
- 错误处理

Q: 能优化性能吗?

直接问:

分析性能瓶颈,提供优化建议

Claude Code会分析代码、识别问题、实施改进。

进阶方向

  • Socket.io实时协作
  • 邮件通知(SendGrid)
  • 任务标签、依赖关系
  • 虚拟滚动、Redis缓存

总结

用Claude Code搭建全栈应用,效率能提升8-10倍。

关键是:

  1. 明确需求 - 说清楚你要什么
  2. 分步实现 - 别一口气做完
  3. 持续测试 - 每个阶段验证
  4. 及时反馈 - 有问题立刻说

Claude Code不只是生成代码,更像一个能理解你意图的编程伙伴。

下一步