快速原型开发
上周我用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 Code | 2-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会自动:
- 创建前后端目录结构
- 生成配置文件
- 初始化package.json
- 创建基础文件
最终的项目结构
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:
- 连接GitHub仓库
- 选择backend目录
- 配置环境变量:
MONGODB_URI=mongodb+srv://...
JWT_SECRET=your-secret
CLIENT_URL=https://your-frontend.vercel.app
NODE_ENV=production
效率对比
| 开发方式 | 总时间 |
|---|---|
| 传统开发 | 17-26小时 |
| Claude Code | 2-3小时 |
快了大概8-10倍。
最终效果
功能清单
- 用户注册登录
- JWT认证
- 完整的CRUD
- 优先级设置
- 状态过滤
- 响应式设计
- 生产部署就绪
使用技巧
清晰描述需求
❌ 不好:
创建一个todo应用
✅ 好:
创建一个todo应用:
- React前端
- Express后端
- MongoDB数据库
- JWT认证
- 完整CRUD
分阶段实现
- 搭建项目结构
- 后端API
- 前端组件
- 前后端联调
- 优化增强
及时测试
每完成一个阶段就测试,有问题直接告诉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倍。
关键是:
- 明确需求 - 说清楚你要什么
- 分步实现 - 别一口气做完
- 持续测试 - 每个阶段验证
- 及时反馈 - 有问题立刻说
Claude Code不只是生成代码,更像一个能理解你意图的编程伙伴。