Flutter + Node.js 全栈实战:从零搭建 RESTful API 后端服务
在全栈开发中,Flutter 负责构建精美的跨平台客户端界面,而 Node.js 则是搭建高性能后端 API 服务的利器。这篇文章记录了我在项目中从零搭建 Node.js 后端并对接 Flutter 客户端的完整过程,涵盖项目结构设计、数据库连接、中间件配置、鉴权机制以及 Flutter 端的 HTTP 请求封装。
技术栈选型
在开始之前,先明确这次实战使用的技术栈。后端采用 Node.js + Express + MongoDB,这是目前最成熟的 JavaScript 全栈组合之一。Express 提供了轻量而灵活的路由框架,MongoDB 作为 NoSQL 数据库在处理非结构化数据时非常方便。鉴权方面使用 JWT (JSON Web Token),密码加密使用 bcryptjs。
Flutter 端使用 dio 作为 HTTP 客户端,它在拦截器、请求取消、FormData 支持等方面远超 Dart 原生的 http 包。
为什么选 Node.js?因为 Flutter 开发者通常已经熟悉 JavaScript/Dart,Node.js 可以让前后端语言统一,降低学习成本。而且 npm 生态极其丰富,几乎所有你需要的中间件都能找到现成的包。
一、后端项目初始化
首先创建项目目录并初始化:
mkdir my-flutter-backend
cd my-flutter-backend
npm init -y
# 安装核心依赖
npm install express mongoose dotenv cors jsonwebtoken bcryptjs
# 安装开发依赖
npm install --save-dev nodemon
安装完成后,项目目录结构如下:
my-flutter-backend/
├── src/
│ ├── config/
│ │ └── db.js # 数据库连接
│ ├── middleware/
│ │ └── auth.js # JWT 鉴权中间件
│ ├── models/
│ │ └── User.js # 用户模型
│ ├── routes/
│ │ ├── auth.js # 认证路由
│ │ └── posts.js # 文章路由
│ ├── controllers/
│ │ ├── authController.js
│ │ └── postController.js
│ ├── utils/
│ │ └── response.js # 统一响应格式
│ └── app.js # Express 应用入口
├── .env
├── server.js # 启动入口
└── package.json
二、数据库连接配置
在 .env 文件中配置环境变量:
PORT=3000
MONGODB_URI=mongodb://localhost:27017/flutter_app
JWT_SECRET=your_super_secret_key_here
JWT_EXPIRES_IN=7d
数据库连接文件 src/config/db.js:
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI);
} catch (error) {
console.error(`数据库连接失败: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
三、用户模型设计
使用 Mongoose 定义用户 Schema,包含邮箱、密码(加密存储)、昵称、头像等字段:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: [true, '请输入用户名'],
unique: true,
trim: true,
minlength: [2, '用户名至少2个字符'],
maxlength: [20, '用户名最多20个字符']
},
email: {
type: String,
required: [true, '请输入邮箱'],
unique: true,
lowercase: true,
match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/, '邮箱格式不正确']
},
password: {
type: String,
required: [true, '请输入密码'],
minlength: 6,
select: false // 默认查询时不返回密码
},
avatar: {
type: String,
default: ''
},
createdAt: {
type: Date,
default: Date.now
}
});
// 保存前自动加密密码
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// 实例方法:验证密码
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
四、JWT 鉴权中间件
鉴权中间件负责从请求头中提取 Token 并验证其有效性,将解码后的用户信息挂载到 req.user 上供后续路由使用:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const protect = async (req, res, next) => {
let token;
// 从 Header 中获取 Bearer Token
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
try {
token = req.headers.authorization.split(' ')[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch (error) {
return res.status(401).json({
success: false,
message: 'Token 无效或已过期'
});
}
}
if (!token) {
return res.status(401).json({
success: false,
message: '未提供认证 Token'
});
}
};
module.exports = protect;
五、统一响应格式
为了让 Flutter 端更容易处理响应,后端统一返回格式化的 JSON 数据:
const successResponse = (res, data, statusCode = 200) => {
res.status(statusCode).json({
success: true,
data,
timestamp: new Date().toISOString()
});
};
const errorResponse = (res, message, statusCode = 500) => {
res.status(statusCode).json({
success: false,
message,
timestamp: new Date().toISOString()
});
};
module.exports = { successResponse, errorResponse };
六、Express 应用入口
将所有组件组装到一起,注册路由和中间件:
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/posts');
const app = express();
// 中间件
app.use(cors({
origin: process.env.CLIENT_URL || '*',
credentials: true
}));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(morgan('dev'));
// 路由
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/posts', postRoutes);
// 健康检查
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', time: new Date().toISOString() });
});
// 404 处理
app.use((req, res) => {
res.status(404).json({ success: false, message: '接口不存在' });
});
// 全局错误处理
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
});
module.exports = app;
七、Flutter 端对接
Flutter 端使用 dio 封装 HTTP 请求。首先定义一个统一的 API 客户端,支持自动注入 Token、统一错误处理:
import 'package:dio/dio.dart';
class ApiClient {
static final ApiClient _instance = ApiClient._internal();
factory ApiClient() => _instance;
late final Dio _dio;
ApiClient._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'http://10.0.2.2:3000/api/v1', // Android 模拟器
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
headers: {'Content-Type': 'application/json'},
));
// 请求拦截器:自动注入 Token
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) async {
final token = await _getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (error, handler) {
// 401 自动跳转登录
if (error.response?.statusCode == 401) {
// 清除本地 Token 并跳转登录页
}
return handler.next(error);
},
));
}
Future<String?> _getToken() async {
// 从 SharedPreferences 或 secure_storage 读取
return null;
}
// GET 请求
Future<Response> get(String path, {Map<String, dynamic>? params}) {
return _dio.get(path, queryParameters: params);
}
// POST 请求
Future<Response> post(String path, {dynamic data}) {
return _dio.post(path, data: data);
}
// PUT 请求
Future<Response> put(String path, {dynamic data}) {
return _dio.put(path, data: data);
}
// DELETE 请求
Future<Response> delete(String path) {
return _dio.delete(path);
}
}
在 Flutter 端调用登录接口的示例:
Future<bool> login(String email, String password) async {
try {
final response = await ApiClient().post('/auth/login', data: {
'email': email,
'password': password,
});
if (response.data['success']) {
final token = response.data['data']['token'];
// 保存 Token 到本地安全存储
await _saveToken(token);
return true;
}
return false;
} on DioException catch (e) {
// 统一错误处理
final message = e.response?.data['message'] ?? '网络异常';
showToast(message);
return false;
}
}
总结
这篇文章覆盖了 Flutter + Node.js 全栈项目后端部分的核心内容:项目结构设计、数据库连接、Mongoose 模型定义、JWT 鉴权中间件、统一响应格式,以及 Flutter 端的 Dio HTTP 客户端封装。整个后端代码不到 300 行,却足以支撑一个完整的移动应用后端服务。
接下来的文章中,我会继续分享文件上传接口的设计、分页查询优化、以及 WebSocket 实时通信的实现。敬请期待。