Serverpod是基于Dart构建的高性能后端框架之一。它是一个功能完备的后端框架,自带对象关系映射层、代码生成系统、数据迁移工具、认证模块以及部署平台。
如果你使用像Shelf这样的工具来构建API,那么你需要自己完成所有环节:选择所需的包,编写中间件,管理数据库连接,并手动将所有组件组装起来。这就是Shelf的使用方式,它能让你深入了解Dart在后端环境中的运作原理。
而Serverpod则采用了完全不同的设计理念。
Shelf只提供基础功能,而Serverpod则能为你提供一个完整的开发系统。你只需使用YAML定义模型,运行代码生成工具,就能自动获得类型化的数据库类、序列化机制以及客户端代码。
对于Flutter工程师来说,这种方式会让他们感到非常熟悉——这其实就是Flutter工具链在后端开发中的体现。
在本文中,我们将使用Serverpod从零开始构建一个用于用户和资料管理的REST API。你将了解到Serverpod的代码生成机制、内置的对象关系映射层以及端点管理系统的运作原理,最终还会掌握如何完成整个后端服务的部署。
目录
先决条件
在开始之前,您需要具备以下条件:
-
熟悉Dart和Flutter开发技术
-
了解REST API的概念、端点、HTTP方法以及状态码
-
已安装并运行Docker Desktop
-
已安装Flutter SDK(即使只是服务器端项目,Serverpod也需要它)
-
拥有Fly.io账户或Serverpod Cloud账户,用于部署项目
Serverpod与Shelf的区别
在编写任何代码之前,了解Shelf与Serverpod在设计理念上的根本差异是非常重要的。这样,你在使用这个框架时做出的每一个设计决策都会显得更加合理、有针对性,而不会显得随意或盲目。
使用Shelf时,你需要亲自完成所有工作:请求解析、响应格式化、数据库查询、数据迁移、身份验证以及日志记录。每一部分代码都是你亲手编写的,因此你非常清楚它们的功能。
而使用Serverpod时,你可以定义一些规则,框架会自动为你生成相应的代码。你只需要用YAML语言定义模型,然后运行`serverpod generate`命令,就能得到一个包含数据库绑定、序列化机制以及客户端访问功能的完整Dart类文件。当你定义了一个端点方法后,框架会负责处理路由分配、参数提取以及响应格式化等工作。
这与Flutter相比也是类似的。Flutter会自动为你编写布局引擎、渲染流程以及手势识别系统的相关代码,让你能够专注于产品的业务逻辑设计。Serverpod在后台也采用了同样的机制。
这种高效性的代价就是灵活性会受到一定限制。Serverpod对于代码的结构有固定的要求;如果你的使用场景符合这些要求,开发速度会非常快;但如果不符合,那么你在使用这个框架时就会遇到很多麻烦。
对于我们正在开发的用户和资料管理API来说,Serverpod是一个非常合适的选择。
安装Serverpod
即使只是进行服务器端开发,也需要先安装Flutter。这是因为在创建项目时,Serverpod的工具链会同时生成客户端包和服务器端包。
全局安装Serverpod CLI:
dart pub global activate serverpod_cli
验证安装是否成功:
serverpod# 应该会显示Serverpod CLI的帮助信息在继续下一步操作之前,请确保Docker Desktop已经处于运行状态。Serverpod会利用Docker来管理本地开发环境中使用的PostgreSQL和Redis数据库。
创建项目
serverpod create user_profile_api cd user_profile_api这条命令会生成三个Dart包:
user_profile_api/ user_profile_api_server/ ← 服务器端代码 user_profile_api_client/ ← 自动生成的客户端代码(请勿修改) user_profile_apiflutter/ ← 预配置好的Flutter应用程序在这篇文章中,我们所编写的所有代码都存储在`user_profile_api_server`目录中。当您需要让Flutter前端与`Serverpod`后端进行交互时,客户端代码以及相关的Flutter包会自动生成并被使用。
了解项目结构
在`user_profile_api_server`目录内部的结构如下:
user_profile_api_server/ bin/ main.dart ← 入口文件 lib/ src/ endpoints/ ← 所有的端点类都存储在这里 generated/ ← 自动生成的代码(切勿手动修改) user_profile_api_server.dart config/ development.yaml ← 数据库和服务器配置文件 staging.yaml production.yaml passwords.yaml ← 数据库密码 migrations/ ← 自动生成的迁移文件 web/ ← 可选的网络服务器相关文件 Dockerfile docker-compose.yaml pubspec.yaml关于这个项目结构,最重要的一点就是`generated`文件夹。其中的所有内容都是由`serverpod generate`命令自动生成的,因此绝对不能手动修改。每当您修改模型或端点配置时,只需重新运行生成工具,该文件夹中的内容就会被完全替换。
`config`文件夹用于存储与特定环境相关的配置信息。`development.yaml`文件已经预先设置了配置项,以便与`Serverpod`在本地创建的Docker容器配合使用。
Serverpod的核心概念
端点与会话对象
在`Serverpod`中,端点是一个继承自`Endpoint`类的实体。该类中的所有公共方法都会被视为客户端可以调用的API接口。无需进行路由配置、处理程序注册或中间件添加等操作——框架会在代码生成过程中自动检测并注册这些端点。
import 'package:serverpod/serverpod.dart'; class UserEndpoint extends Endpoint { Futuregreet(Session session, String name) async { return 'Hello, $name!'; } } “会话对象”是`Serverpod`中最为重要的组件。它会被传递给所有的端点方法,使您能够访问以下资源:
-
`session.db`:用于执行数据库操作
-
`session.auth`:包含认证相关信息
-
`session.log`:用于结构化日志记录
-
`session.caches`:用于缓存数据
-
`session.messages`:用于实现实时消息传递功能
可以把“会话对象”看作是`Serverpod`中相当于Flutter中的`BuildContext`的部分。它是访问框架提供的一切功能的入口,而且在所有相关函数中总是作为第一个参数被传递。
模型文件与代码生成
在这一点上,`Serverpod`与`Shelf`有着明显的区别。您无需手动编写Dart模型类,只需在`.spy.yaml`文件中定义数据结构,`Serverpod`会自动为您生成相应的Dart代码。
公司的模型文件格式如下:
class: Company
table: company
fields:
name: String
foundedDate: DateTime?
运行 `serverpod generate` 命令后,会生成一个完整的 Dart 类,其中包含:
-
类型正确的不可变字段
-
用于序列化的 `toJson` 和 `fromJson` 方法
-
通过 `db` 静态访问器实现的数据库绑定功能
-
构造函数以及 `copyWith` 方法
-
在客户端包中也会生成相同的类,因此 Flutter 应用可以直接使用这些类
这就是这种工具所带来的核心优势:你只需在 YAML 中定义一次模型结构,就能得到一个在服务器、数据库和客户端端都能一致使用的、类型明确的模型,而且完全不会出现重复代码的情况。
内置的 ORM
Serverpod 的 ORM 直接使用生成的模型类。所有的数据库操作都是通过模型上的 `db` 静态访问器来执行的:
// 插入一条记录
var company = Company(name: 'Serverpod Corp', foundedDate: DateTime.now());
company = await Company.db.insertRow(session, company);
// 按 ID 查找
var found = await Company.db.findByIdsession, company.id!);
// 根据条件查找
var result = await Company.db.findFirstRow(
session,
where: (t) => t.name.equals('Serverpod Corp'),
);
// 找出所有记录并按名称排序
var all = await Company.db.find(
session,
orderBy: (t) => t.name,
);
// 更新记录
company = company.copyWith(name: 'New Name');
await Company.db.updateRow(session, company);
// 删除记录
await Company.db.deleteRow(session, company);
`where` 参数使用了类型安全的表达式构建器。`t` 参数允许你以类型安全的方式访问数据库表的列,因此查询条件可以享受自动补全功能,并且会在编译时进行检查。这样就无需使用原始 SQL 语句,也不需要使用基于字符串的列名,从而避免了运行时出现意外情况。
迁移操作
当你修改模型结构时,Serverpod 会自动生成相应的迁移脚本:
serverpod create-migration
这样就会在 `migrations/` 目录下生成一个 SQL 迁移文件。在启动服务器时需要执行这个命令:
dart bin/main.dart --apply-migrations
Serverpod 会记录哪些迁移脚本已经执行过,因此只会运行新的迁移脚本。这种迁移系统与模型系统实现了完全集成,因此你的 Dart 类与数据库模式之间不会出现任何不一致的情况。
启动开发服务器
在编写任何代码之前,首先需要启动开发环境。
启动 Docker 容器(PostgreSQL 和 Redis):
cd user_profile_api_server
docker compose up --build --detach
在应用了迁移脚本之后再启动服务器:
dart bin/main.dart --apply-migrations
此时你应该能够看到服务器已经成功启动并运行了。
SERVERPOD版本:2.x.x,运行模式:开发模式
Insights服务监听端口为8081;
服务器默认监听端口为8080;
Web服务器监听端口为8082。
有三个端口:端口8080是主要的API服务器;端口8081用于使用Serverpod Insights工具进行监控;端口8082则是一个可选的Web服务器。在本文中,我们将专门使用端口8080。
定义模型
用户模型
在服务器包中创建文件`lib/src/models/userspy.yaml`:
class: AppUser
table: app_users
fields:
email: String
passwordHash: String
firstName: String
lastName: String
isActive: bool, default=true
indexes:
app_users_email_idx:
fields: email
unique: true
这里有几点需要注意:该类的名称被定义为`AppUser`,而不是`User`,这是为了避免与Serverpod认证模块中内置的`User`类发生冲突。`table`字段用于指定PostgreSQL数据库中的表名,而`indexes`块则会为`email`列创建一个唯一索引,从而在数据库层面上确保该字段的值是唯一的。
Serverpod会自动为所有具有`table`字段的模型添加一个类型为`int?`的`id`字段,因此你无需自行声明这个字段。
个人资料模型
创建文件`lib/src/models/profilespy.yaml`:
class: Profile
table: profiles
fields:
userId: int
bio: String?
avatarUrl: String?
phone: String?
location: String?
website: String?
indexes:
profiles_user_id_idx:
fields: userId
unique: true
`userId`是一个整数,用于引用`AppUser`对象的ID。由于Serverpod的模型系统在YAML格式中还不支持外键声明语法,因此关联关系的完整性是在应用程序层通过端点逻辑来处理的。
运行代码生成脚本
当两个模型文件都准备就绪后,运行代码生成工具:
serverpod generate
这样就会在`lib/src/generated/`目录下生成Dart类文件。对于`AppUser`模型,生成的代码如下:
// 这些代码是自动生成的,请勿直接修改
class AppUser extends SerializableEntity {
AppUser({
this.id,
required this.email,
required this.passwordHash,
required this.firstName,
required this.lastName,
this.isActive = true,
});
int? id;
String email;
String passwordHash;
String firstName;
String lastName;
bool isActive;
// 用于ORM操作的数据库访问接口
static final db = AppUserRepository._();
// 序列化方法
factory AppUser.fromJson(Map jsonSerialization, ...) { ... }
Map toJson() { ... }
}
生成的代码就是你的端点程序所用来与数据库交互的部分,你绝对不需要手动编写这些代码。
创建并应用迁移脚本
在模型文件生成完成后,接下来需要创建相应的迁移脚本:
serverpod create-migration
这样就会在`migrations/`目录下生成带有时间戳的SQL脚本文件。接下来需要应用这些迁移脚本:
dart bin/main.dart --apply-migrations
您的`app_users`表和`profiles`表现在已经存在于PostgreSQL数据库中,且包含了正确的列和索引。
构建API
认证端点
创建文件`lib/src/endpoints/auth_endpoint.dart`:
import 'package:serverpod/serverpod.dart';
import 'package:bcrypt/bcrypt.dart';
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import '../generated/protocol.dart';
class AuthEndpoint extends Endpoint {
Future
Serverpod的端点会返回具有明确数据类型的值。当您返回一个`Map
用户端点
创建文件 `lib/src/endpoints/user_endpoint.dart`:
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart';
class UserEndpoint extends Endpoint {
@override
bool get requireLogin => true;
Future>> getAll(Session session) async {
final users = await AppUser.db.find(
session,
where: (t) => t.isActive.equals(true),
orderBy: (t) => t.id,
);
return users.map(_sanitizeUser).ToList();
}
Future> getById(Session session, int userId) async {
final user = await AppUser.db.findById(session, userId);
if (user == null || !user.isActive) {
throw Exception('用户未找到');
}
return _sanitizeUser(user);
}
Future> update(
Session session,
int userId,
String? firstName,
String? lastName,
) async {
final user = await AppUser.db.findById(session, userId);
if (user == null || !user.isActive) {
throw Exception('用户未找到');
}
final updated = user.copyWith(
firstName: firstName ?? user.firstName,
lastName: lastName ?? user.lastName,
);
await AppUser.db.updateRow(session, updated);
return _sanitizeUser(updated);
}
Future delete(Session session, int userId) async {
final user = await AppUser.db.findById(session, userId);
if (user == null || !user.isActive) {
throw Exception('用户未找到');
}
// 软删除
final deactivated = user.copyWith(isActive: false);
await AppUser.db.updateRow(session, deactivated);
}
Map _sanitizeUser(AppUser user) => {
'id': user.id,
'email': user.email,
'firstName': user.firstName,
'lastName': user.lastName,
'isActive': user.isActive,
};
}
注意 `@override bool get requireLogin => true` 这一行。这是 Serverpod 为保护端点而提供的内置机制。当这个获取方法返回 `true` 时,Serverpod 会在调用该方法之前,对每个发送到该端点的请求进行身份验证。未经授权的请求会被框架自动拒绝。
个人资料端点
创建文件 `lib/src/endpoints/profile_endpoint.dart`:
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart';
class ProfileEndpoint extends Endpoint {
@override
bool get requireLogin => true;
Future> getByUserId(
Session session,
int userId,
) async {
final user = await AppUser.db.findById(session, userId);
if (user == null || !user.isActive) {
throw Exception('用户未找到');
}
final profile = await Profile.db.findFirstRow(
session,
where: (t) => t.userId.equals(userId),
);
if (profile == null) {
throw Exception('个人资料未找到');
}
return _profileToMap(profile);
}
Future> create(
Session session,
int userId,
String? bio,
String? avatarUrl,
String? phone,
String? location,
String? website,
) async {
final user = await AppUser.db.findById(session, userId);
if (user == null || !user.isActive) {
throw Exception('用户未找到');
}
final existing = await Profile.db.findFirstRow(
session,
where: (t) => t.userId.equals(userId),
);
if (existing != null) {
throw Exception('该用户的个人资料已经存在');
}
var profile = Profile(
userId: userId,
bio: bio,
avatarUrl: avatarUrl,
phone: phone,
location: location,
website: website,
);
profile = await Profile.db.insertRow(session, profile);
return _profileToMap(profile);
}
Future> update(
Session session,
int userId,
String? bio,
String? avatarUrl,
String? phone,
String? location,
String? website,
) async {
final profile = await Profile.db.findFirstRow(
session,
where: (t) => t.userId.equals(userId),
);
if (profile == null) {
throw Exception('个人资料未找到');
}
final updated = profile.copyWith(
bio: bio ?? profile.bio,
avatarUrl: avatarUrl ?? profile.avatarUrl,
phone: phone ?? profile.phone,
location: location ?? profile.location,
website: website ?? profile.website,
);
await Profile.db.updateRow(session, updated);
return _profileToMap(updated);
}
Map _profileToMap(Profile profile) => {
'id': profile.id,
'userId': profile.userId,
'bio': profile.bio,
'avatarUrl': profile.avatarUrl,
'phone': profile.phone,
'location': profile.location,
'website': profile.website,
};
}
添加这些端点后,再次运行生成器,以便Serverpod能够将其注册起来:
serverpod generate
身份验证
密码哈希与JWT
在server包的pubspec.yaml文件中添加所需的依赖包:
dependencies:
serverpod: ^2.5.0
bcrypt: ^1.1.3
dart_jsonwebtoken: ^2.12.0
然后运行dart pub get命令。
在auth端点中,_generateToken和sanitizeUser这两个辅助函数负责处理密码哈希及JWT的生成。JWT密钥是通过Session.serverpod.password('jwtSecret')从Serverpod内置的密码管理系统中获取的。
将这个密钥添加到config/passwords.yaml文件中:
development:
database: 'dart_password'
jwtSecret: '你的开发环境JWT密钥'
在Serverpod项目中,这个文件默认会被包含在.gitignore文件中。生产环境的密钥则通过环境变量或Serverpod Cloud的秘密管理功能来配置。
保护端点
Serverpod提供了两层端点保护机制:
requireLogin——自动拒绝未经身份验证的请求:
@override
bool get requireLogin => true;
requiredScopes——要求具备特定的权限范围:
@override
Set get requiredScopes => {Scope.admin};
对于本文中提到的User和Profile端点来说,使用requireLogin就足够了。登录响应中返回的token会在后续的所有请求中通过Authorization头部传递,Serverpod会在调用端点方法之前验证这个token的有效性。
在端点内部验证token,以获取当前用户的ID:
Future someProtectedMethod(Session session) async {
final authInfo = await session.authenticated;
if (authInfo == null) {
throw Exception('未通过身份验证');
}
final userId = authInfo.userId;
// 使用userId继续执行后续操作
}
Serverpod中的错误处理
Serverpod会自动将端点方法中抛出的异常转换为结构化的错误响应。当你抛出异常时:
throw Exception('用户未找到');
客户端会收到一个结构化错误响应。为了实现更细粒度的控制,Serverpod还提供了类型化的异常处理方式:
throw ServerpodClientException('用户未找到', statusCode: 404);
对于服务器端的日志记录操作,如果不需要向客户端暴露详细信息,可以这样处理:
session.log('在创建用户过程中出现了意外错误', level: LogLevel.error);
throw Exception('发生了内部错误');
Serverpod的日志系统会将日志存储在数据库中,用户可以通过端口8081上的Insights控制面板来查询这些日志。所有请求都会被自动记录下来,其中会包含时间信息、请求端点的名称以及请求的结果,因此无需使用任何额外的中间件。
测试API
Serverpod通过HTTP提供其接口端点。你可以直接使用curl来测试这些接口,不过请求格式遵循的是Serverpod的RPC规范,而不是传统的REST结构。
对于某个接口方法来说,生成的URL格式如下:
POST /[endpoint]/[method]
请求体需要包含该方法所需的参数,且数据格式应为JSON。
注册用户示例:
curl http://localhost:8080/auth/register \
-X POST \
-H "Content-Type: application/json" \
-d '{
"email": "seyi@example.com",
"password": "securepassword",
"firstName": "Seyi",
"lastName": "Dev"
}'
登录用户示例:
curl http://localhost:8080/auth/login \
-X POST \
-H "Content-Type: application/json" \
-d '{"email": "seyi@example.com", "password": "securepassword"}'
获取所有已认证用户信息:
curl http://localhost:8080/user/getAll \
-X POST \
-H "Authorization: Bearer eyJhbGci..."
根据ID查询用户信息:
curl http://localhost:8080/user/getById \
-X POST \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"userId": 1}'
创建用户个人资料:
curl http://localhost:8080/profile/create \
-X POST \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"bio": "从Flutter工程师转型为后端开发人员",
"location": "尼日利亚拉各斯",
"website": "https://example.com"
}'
更新用户信息:
curl http://localhost:8080/user/update \
-X POST \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"userId": 1, "firstName": "Oluwaseyi"}'
删除用户信息:
curl http://localhost:8080/user/delete \
-X POST \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{"userId": 1}'
部署
使用Docker和Fly.io进行部署
在创建项目时,Serverpod会自动生成一个Dockerfile,该文件位于`user_profile_api_server/Dockerfile`目录中,可以直接使用。
服务器包中包含的`docker-compose.yaml`文件用于管理本地开发环境中的PostgreSQL和Redis数据库。而在Fly.io上进行生产环境部署时,也会遵循基于Docker的相同部署流程。
步骤1 — 使用Fly进行身份验证:
fly auth login
步骤2 — 从服务器目录启动应用程序:
cd user_profile_api_server
fly launch
步骤3 — 设置生产环境所需的秘密信息:
fly secrets set JWT_SECRET="你的生产环境JWT密钥"
步骤4 — 更新生产环境配置:
使用Fly提供的数据库连接信息编辑config/production.yaml文件。Fly会自动设置DATABASE_URL环境变量,而你需要将其映射到Serverpod的配置格式中。
步骤5 — 部署应用:
fly deploy
步骤6 — 在首次部署时应用数据库迁移脚本:
fly ssh console
dart bin/main.dart --apply-migrations --mode production
使用Serverpod Cloud进行部署
Serverpod Cloud是专为Serverpod应用程序设计的原生部署平台。它能够以最少的配置完成数据库的配置、扩展、监控以及部署工作。
首先安装Serverpod Cloud命令行工具:
dart pub global activate serverpod_cloud_cli
接下来进行身份验证:
scloud login
在cloud.serverpod.dev网站上创建一个项目,然后将你的本地项目与之关联:
scloud link --project-id 你的项目ID
最后进行部署:
scloud deploy
Serverpod Cloud会自动为你配置一个托管型的PostgreSQL数据库,应用你定义的迁移脚本,并完成应用的部署工作。此外,它还提供了Insights监控面板,用于实时查看生产环境中的请求情况、日志数据以及性能指标。
结论
与Shelf相比,Serverpod采用了完全不同的设计理念。Shelf注重赋予用户更多的控制权,而Serverpod则更强调效率。你只需使用YAML格式定义模型,运行生成工具,系统就会自动生成数据库相关的类、序列化代码以及客户端代码。你只需要编写端点处理方法,框架会负责处理路由分配、参数提取、身份验证以及错误信息的处理等工作。
ORM功能是Serverpod最强大的优势之一。类型安全的查询语句、自动生成的迁移脚本,以及代码与数据库模式之间的高度一致性,使得使用Serverpod进行数据库操作的速度明显更快,安全性也更高。
对于那些需要为多个客户端提供服务、需要遵循外部REST规范,或者需要与不支持Serverpod格式的现有基础设施进行集成的API来说,像Shelf这样的低级工具能让你拥有更大的控制权。如果你想了解如何使用Shelf来实现相同的用户和资料管理功能,并直接对比这两种开发方法,可以在这里阅读相关文章。
知道哪种工具适合哪项任务,正是区分那些仅仅熟悉某个框架的开发者与真正理解后端开发原理的开发者的关键所在。
祝编码愉快!




