1. 从零搭 App 全栈骨架:前端、后端、数据库与用户界面怎么串起来
很多人第一次做 App 全栈,卡的不是某个单点技术,而是「前端页面、后端接口、数据库读写、用户界面」这四块怎么拼成一个能跑起来的最小闭环。你可能已经看过 React Native 的 Hello World、Express 的几行服务器代码、Mongoose 的模型定义,但把它们放在一个项目里,启动顺序、端口、请求路径、数据格式一乱,就报错。
这篇给的是一个最小可运行骨架:一个待办清单 App,前端用 React Native(Expo 方式启动,省去原生环境折腾),后端用 Node.js + Express,数据库用 MongoDB + Mongoose,用户界面就是列表 + 输入框 + 按钮。同时把 TaoToken 统一 Key 接进后端,让「AI 生成待办建议」这个接口一次跑通。TaoToken 是一个统一的大模型 API 通道,你拿一个 Key 就能调用多种模型,适合在 App 后端里做 AI 能力接入,不用为每个模型单独配一套鉴权。
适合谁看:会一点 JavaScript、想跑通第一个全栈闭环的人;或者已经会写单块代码、但没串过完整链路的人。整篇按「先跑通再优化」的思路走,每一步都有可复制的命令和配置,最后给自测验证和报错排查。
我试过把这套骨架拆给几个刚入门的朋友,他们最容易忽略的是后端要先于前端启动,以及前端请求的地址不能写 localhost(真机调试时 localhost 指向手机自己)。这两个坑后面会专门讲。
先明确目录结构,这是后面所有配置的基准:
my-fullstack-app/ ├── backend/ │ ├── src/ │ │ ├── index.js │ │ ├── db.js │ │ ├── models/Todo.js │ │ └── routes/todo.js │ ├── .env │ └── package.json └── frontend/ ├── App.js ├── api/client.js └── package.json后端负责接口和数据库,前端负责界面和请求。TaoToken 的 Key 只放在后端.env里,前端永远不碰 Key,这是安全底线。
2. TaoToken 前置准备:统一 Key 与 API 通道配置
在写业务代码之前,先把 TaoToken 的通道配好,否则后面 AI 接口会一直 401。TaoToken 官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基础地址是 https://taotoken.net/api (这个地址不加 UTM 参数,直接用于代码里的 base_url)。
你需要做三件事:注册账号、创建 API Key、确认要用的模型 ID。登录后进控制台,在 API Keys 页面新建一个 Key,复制出来(只显示一次,丢了就重建)。模型 ID 在文档的模型列表里能查到,比如常见的对话模型 ID。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Keys 页面是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
拿到 Key 后,后端.env这样写:
# backend/.env PORT=3000 MONGO_URI=mongodb://127.0.0.1:27017/fullstack_demo TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=你的模型ID这里有个关键点:TaoToken 的接口是 OpenAI 兼容格式,所以后端可以直接用openai这个 npm 包,只要把baseURL指向 TaoToken 的 API 地址即可。这样你不需要为 TaoToken 单独写一套 HTTP 请求逻辑,省很多事。
注意:Key 只放后端,前端通过自己的后端接口间接调用 AI。如果你把 Key 写进前端代码,打包后任何人都能扒出来,这是最常见的泄露方式。
依赖清单(后端):
cd backend npm init -y npm install express mongoose dotenv cors openai npm install -D nodemoncors是为了让前端跨域请求后端,dotenv读.env,openai用来调 TaoToken 通道。装完后package.json里加启动脚本:
{ "scripts": { "dev": "nodemon src/index.js", "start": "node src/index.js" } }前端依赖(Expo 方式):
npx create-expo-app frontend cd frontend npm install axiosExpo 的好处是不用装 Android Studio 或 Xcode 就能在手机 Expo Go 里预览,适合快速验证全栈链路。到这里前置就绪,下一节开始写可复制的配置和代码。
3. 可复制配置:后端接口、数据库模型与前端请求
这一节是核心,按「数据库 → 后端 → 前端」的顺序写,每段都能直接复制。
先写数据库连接backend/src/db.js:
const mongoose = require('mongoose'); async function connectDB() { try { await mongoose.connect(process.env.MONGO_URI); console.log('MongoDB connected'); } catch (err) { console.error('MongoDB connect failed:', err.message); process.exit(1); } } module.exports = connectDB;再写数据模型backend/src/models/Todo.js:
const mongoose = require('mongoose'); const todoSchema = new mongoose.Schema({ title: { type: String, required: true }, done: { type: Boolean, default: false }, createdAt: { type: Date, default: Date.now } }); module.exports = mongoose.model('Todo', todoSchema);接着写路由backend/src/routes/todo.js,包含增删查和 AI 建议接口:
const express = require('express'); const router = express.Router(); const Todo = require('../models/Todo'); const OpenAI = require('openai'); const client = new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL }); router.get('/', async (req, res) => { const todos = await Todo.find().sort({ createdAt: -1 }); res.json(todos); }); router.post('/', async (req, res) => { const todo = await Todo.create({ title: req.body.title }); res.status(201).json(todo); }); router.patch('/:id', async (req, res) => { const todo = await Todo.findByIdAndUpdate( req.params.id, { done: req.body.done }, { new: true } ); res.json(todo); }); router.post('/ai-suggest', async (req, res) => { try { const completion = await client.chat.completions.create({ model: process.env.TAOTOKEN_MODEL, messages: [ { role: 'system', content: '你是一个待办助手,只输出一条简短待办。' }, { role: 'user', content: req.body.prompt || '给我一条今天的待办' } ] }); res.json({ suggestion: completion.choices[0].message.content }); } catch (err) { res.status(500).json({ error: err.message }); } }); module.exports = router;入口backend/src/index.js:
require('dotenv').config(); const express = require('express'); const cors = require('cors'); const connectDB = require('./db'); const todoRoutes = require('./routes/todo'); const app = express(); app.use(cors()); app.use(express.json()); app.use('/api/todos', todoRoutes); const PORT = process.env.PORT || 3000; connectDB().then(() => { app.listen(PORT, () => console.log(`Server on ${PORT}`)); });前端请求封装frontend/api/client.js:
import axios from 'axios'; // 真机调试时把 localhost 换成电脑局域网 IP,如 192.168.1.10 const BASE_URL = 'http://192.168.1.10:3000/api'; export const getTodos = () => axios.get(`${BASE_URL}/todos`); export const addTodo = (title) => axios.post(`${BASE_URL}/todos`, { title }); export const toggleTodo = (id, done) => axios.patch(`${BASE_URL}/todos/${id}`, { done }); export const aiSuggest = (prompt) => axios.post(`${BASE_URL}/todos/ai-suggest`, { prompt });前端界面frontend/App.js:
import React, { useEffect, useState } from 'react'; import { View, Text, TextInput, Button, FlatList, TouchableOpacity } from 'react-native'; import { getTodos, addTodo, toggleTodo, aiSuggest } from './api/client'; export default function App() { const [todos, setTodos] = useState([]); const [text, setText] = useState(''); const load = async () => setTodos((await getTodos()).data); useEffect(() => { load(); }, []); const handleAdd = async () => { if (!text.trim()) return; await addTodo(text); setText(''); load(); }; const handleAI = async () => { const res = await aiSuggest('给我一条今天的待办'); setText(res.data.suggestion); }; return ( <View style={{ padding: 24, marginTop: 60 }}> <Text style={{ fontSize: 22, marginBottom: 12 }}>我的待办</Text> <TextInput value={text} onChangeText={setText} placeholder="输入待办" style={{ borderWidth: 1, padding: 8, marginBottom: 8 }} /> <Button title="添加" onPress={handleAdd} /> <Button title="AI 建议" onPress={handleAI} /> <FlatList data={todos} keyExtractor={(item) => item._id} renderItem={({ item }) => ( <TouchableOpacity onPress={() => toggleTodo(item._id, !item.done).then(load)}> <Text style={{ padding: 8, textDecorationLine: item.done ? 'line-through' : 'none' }}> {item.title} </Text> </TouchableOpacity> )} /> </View> ); }这套配置里,TaoToken 的三件套是完整的:Base URL 用https://taotoken.net/api,Key 用.env里的TAOTOKEN_API_KEY,Model ID 用TAOTOKEN_MODEL。三者缺一,AI 接口就会报错。
4. 启动与验证:接口自测和成功结果长什么样
启动顺序很重要:先数据库,再后端,最后前端。
第一步,确保本地 MongoDB 在跑。如果你用 Docker,一条命令:
docker run -d -p 27017:27017 --name mongo-demo mongo:7第二步,启动后端:
cd backend npm run dev看到MongoDB connected和Server on 3000就对了。
第三步,用 curl 自测接口,先测数据库读写:
curl -X POST http://localhost:3000/api/todos \ -H "Content-Type: application/json" \ -d '{"title":"写全栈骨架"}' curl http://localhost:3000/api/todos第二条应该返回一个数组,里面有刚创建的待办,带_id、title、done、createdAt。这说明前端 → 后端 → 数据库的链路通了。
第四步,测 TaoToken 的 AI 接口:
curl -X POST http://localhost:3000/api/todos/ai-suggest \ -H "Content-Type: application/json" \ -d '{"prompt":"给我一条今天的待办"}'成功时返回类似:
{"suggestion":"整理今天的会议纪要并同步给团队"}如果这一步返回了内容,说明 TaoToken 统一 Key 通道接入成功,后端能正常调用模型。
第五步,启动前端:
cd frontend npx expo start用手机 Expo Go 扫码,或者按w在浏览器预览。界面上输入待办、点添加,列表会刷新;点「AI 建议」,输入框会自动填入模型返回的待办。到这里,前端页面、后端接口、数据库读写、用户界面四块就串成一个闭环了。
提示:真机调试时,
api/client.js里的BASE_URL必须换成电脑的局域网 IP,手机和电脑要在同一个 Wi-Fi 下。用localhost手机会请求自己,必然失败。
验证通过后,你可以把 AI 建议接口换成更复杂的 prompt,比如根据已有待办生成优先级排序,TaoToken 通道本身不用改,只改 messages 内容即可。
5. 常见报错排查:401、local proxy failed、reading choices 怎么解
这一节按真实报错来,每个都给定位方法和修复。
报错一:401 Unauthorized。这是 TaoToken Key 的问题。先确认.env里TAOTOKEN_API_KEY没有多余空格或引号,再确认后端启动时dotenv真的读到了(可以在index.js里临时console.log(process.env.TAOTOKEN_API_KEY?.slice(0,6)))。如果 Key 是对的还 401,去控制台看这个 Key 是否被禁用或额度用尽。修复就是重建 Key 并更新.env,重启后端。
报错二:local proxy failed 或连接超时。这类通常是baseURL写错。确认代码里是https://taotoken.net/api,不要多加/v1或结尾斜杠。如果你在openai包里传了baseURL,SDK 会自动拼/chat/completions,手动加路径反而会 404。另外确认本机网络能正常访问该地址,公司网络如果有出口限制,换网络再试。
报错三:Cannot read properties of undefined (reading 'choices')。这说明completion.choices是 undefined,通常是接口返回了错误对象而不是正常响应。把catch里的err.message打全,或者打印err.response?.data,就能看到真实原因,多半还是 Key 或模型 ID 问题。确认TAOTOKEN_MODEL填的是文档里真实存在的模型 ID,拼错一个字符就会报模型不存在。
报错四:MongoDB connect failed。检查MONGO_URI里的端口和容器是否在跑,docker ps看mongo-demo是否 Up。如果用了127.0.0.1还连不上,试试localhost,某些环境解析不同。
报错五:前端请求 Network Error。九成是BASE_URL用了localhost。真机改成电脑局域网 IP,并确认后端cors()已启用。如果还不行,在电脑浏览器访问http://电脑IP:3000/api/todos,能返回数据说明后端没问题,问题在前端地址。
报错六:OAuth 相关报错。如果你在别处配过 OAuth 或 Claude Code 之类的工具,注意 TaoToken 的 Key 是 API Key 模式,不走 OAuth 流程。把鉴权方式统一成Authorization: Bearer <Key>,不要混用。
排查顺序建议:先 curl 后端接口,再 curl AI 接口,最后才看前端。这样能快速定位是数据库、TaoToken 通道还是前端请求的问题。
6. 把骨架用起来:下一步接什么、Key 怎么管
跑通之后,这个骨架可以往几个方向长。第一,把 AI 建议接口做成「根据当前待办列表生成今日计划」,prompt 里带上todos的内容,TaoToken 通道不用动。第二,加用户体系,用 JWT 做登录,Todo 模型加userId字段,接口按用户过滤。第三,把前端从 Expo 切到正式打包,或者换成 Flutter,后端接口完全不用改,这就是前后端分离的好处。
Key 管理上,长期项目建议用 Coding Plan 这类方案来统一管理调用额度,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,适合需要持续调用模型的场景。如果你只是想先验证模型效果,可以直接在模型对话页面试,地址是 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到接口细节问题先查文档。
最后给一个实用技巧:把.env加进.gitignore,永远不要提交 Key。团队协作时,每个人用自己的 Key,通过环境变量注入,代码里只留process.env.TAOTOKEN_API_KEY。这样既安全,也方便换 Key 不用改代码。骨架跑通只是开始,真正省时间的是把配置和密钥管理规范化,后面加功能才不会乱。