T
traeai
登录
返回首页
freeCodeCamp.org

从 Flutter 到后端:如何使用 Dart 和 Serverpod 构建生产级 REST API

8.7Score
从 Flutter 到后端:如何使用 Dart 和 Serverpod 构建生产级 REST API

TL;DR · AI 摘要

本文介绍了如何使用 Dart 和 Serverpod 构建生产级 REST API,对比了 Serverpod 与 Shelf 的开发哲学,并通过实例演示了从项目创建到代码生成、模型定义和 API 实现的完整流程。

核心要点

  • Serverpod 是一个全栈后端框架,提供代码生成、ORM 和部署平台,适合快速开发。
  • 与 Shelf 手动组装组件不同,Serverpod 通过 YAML 定义模型自动生成代码,提升开发效率。
  • Serverpod 要求 Flutter SDK 安装,用于生成客户端代码并与 Flutter 前端集成。

结构提纲

按章节快速跳转。

  1. 介绍 Serverpod 后端框架及其与传统手动构建方式(如 Shelf)的区别,强调其生产力优势。

  2. 详细说明 Serverpod 提供的代码生成、ORM、认证模块和部署平台等功能。

  3. 列出构建 Serverpod 项目的前提条件,包括 Dart/Flutter 安装、Docker 和部署账户。

  4. 演示如何使用 Serverpod CLI 创建项目,并解释生成的三个 Dart 包的功能。

  5. 展示如何通过 YAML 定义数据模型,以及 Serverpod 如何自动生成数据库类和客户端代码。

  6. 讲解如何实现 REST API 端点,并最终完成项目的部署。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 构建生产级 REST API
    • Serverpod 特性
      • 代码生成
      • ORM
      • 认证模块
      • 部署平台
    • 开发环境
      • Dart/Flutter
      • Docker
      • Fly.io/Serverpod Cloud
    • 项目结构
      • server 包
      • client 包
      • Flutter 包
    • 模型定义
      • YAML 配置
      • 自动代码生成
    • API 实现
      • 端点定义
      • 路由与参数处理

金句 / Highlights

值得收藏与分享的关键句。

  • Serverpod 是一个全栈后端框架,提供 ORM、代码生成、迁移工具、认证模块和部署平台。

    第 2 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • 与 Shelf 手动组装组件不同,Serverpod 通过 YAML 定义模型自动生成代码,提升开发效率。

    第 4 段

    ⬇︎ 下载 PNG𝕏 分享到 X
  • Serverpod 要求 Flutter SDK 安装,用于生成客户端代码并与 Flutter 前端集成。

    第 6 段

    ⬇︎ 下载 PNG𝕏 分享到 X
#Dart#Serverpod#REST API#后端#Flutter
打开原文

从 Flutter 到后端:如何使用 Dart 和 Serverpod 构建生产级 REST API

源链接

发布时间:2026-06-02T16:25:50.551Z

图像

图像 1:从 Flutter 到后端:如何使用 Dart 和 Serverpod 构建生产级 REST API

Serverpod 是基于 Dart 构建的最高效的后端框架之一。它是一个完全有观点的后端框架,自带 ORM、代码生成系统、迁移工具、认证模块和部署平台。

如果你使用像 Shelf 这样的工具来构建你的 API,你需要自己组装所有内容。你选择自己的包,编写自己的中间件,管理自己的数据库连接,并手动将每个部分连接起来。这就是 Shelf 的方式,它让你深入了解服务器端 Dart 的内部工作原理。

Serverpod 则完全不同。

Shelf 给你的是基础组件,而 Serverpod 提供的是一个完整的系统。你用 YAML 定义模型,运行生成器,然后自动生成完全类型化的数据库类、序列化代码和客户端代码。

对于 Flutter 工程师来说,这感觉非常熟悉。它与 Flutter 工具链本身带来的生产力是相同的,只是应用到了后端。

在本文中,我们将从头开始构建一个用户和资料管理 REST API,使用 Serverpod。你将学习 Serverpod 的代码生成、内置 ORM 和端点系统的工作原理,并最终部署一个完整的后端。

目录

前提条件

在开始之前,你应该具备以下条件:

  • 熟悉 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 相比于直接使用原生平台 API 的权衡是一样的。Flutter 为你编写布局引擎、渲染管道和手势系统。你专注于产品逻辑。Serverpod 在后端做出了同样的选择。

这种生产力的代价是灵活性。Serverpod 对事物应该如何结构化有着强烈的观点。如果你的用例符合这些观点,开发速度将非常快。如果不符合,你就是在与框架对抗。

对于我们正在构建的用户和资料管理 API 来说,Serverpod 是一个非常好的选择。

安装 Serverpod

Serverpod 需要安装 Flutter,即使是纯服务器工作也是如此。这是因为它的工具链在项目创建过程中会同时构建客户端包和服务器包。

全局安装 Serverpod CLI:

bash
dart pub global activate serverpod_cli

验证安装:

bash
serverpod
# 应该打印出 Serverpod CLI 的帮助信息

在继续之前,请确保 Docker Desktop 正在运行。Serverpod 使用 Docker 来管理本地开发中的 PostgreSQL 和 Redis。

创建项目

bash
serverpod create user_profile_api
cd user_profile_api

这个命令会创建三个 Dart 包:

bash
user_profile_api/
  user_profile_api_server/ 你的服务器代码
  user_profile_api_client/ 自动生成的客户端(不要编辑)
  user_profile_api_flutter/ 预配置好的 Flutter 应用,用于连接

对于本文,我们编写的代码都将位于 user_profile_api_server 中。客户端和 Flutter 包是自动生成的,并且当你需要一个 Flutter 前端与你的 Serverpod 后端通信时会用到它们。

理解项目结构

user_profile_api_server 内部:

bash
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/ 可选的 Web 服务器文件
  Dockerfile
  docker-compose.yaml
  pubspec.yaml

关于此结构最重要的一点是 generated/ 文件夹。其中的所有内容都是由 serverpod generate 生成的,永远不应该手动编辑。当你更改模型或端点时,运行生成器会完全重写该文件夹。

config/ 文件夹包含环境特定的配置。development.yaml 文件已预先配置为与 Serverpod 在本地启动的 Docker 容器一起工作。

Serverpod 核心概念

端点和会话对象

在 Serverpod 中,一个端点是一个继承自 Endpoint 的类。该类中的每个公共方法都会成为一个客户端可以调用的 API 调用。无需进行路由配置、处理器注册或中间件挂载。框架会在代码生成期间自动发现并注册你的端点。

dart
import 'package:serverpod/serverpod.dart';

class UserEndpoint extends Endpoint {
  Future<String> greet(Session session, String name) async {
    return 'Hello, $name!';
  }
}

Session 对象是 Serverpod 中最重要的参数。它会被传递给每个端点方法,并提供以下访问权限:

  • session.db 用于数据库操作
  • session.auth 用于认证信息
  • session.log 用于结构化日志记录
  • session.caches 用于缓存
  • session.messages 用于实时消息

可以将 Session 视为 Serverpod 的等价物,类似于 Flutter 中的 BuildContext。它是访问框架所提供的一切功能的入口,并且始终是第一个参数。

模型文件与代码生成

这是 Serverpod 的方法与 Shelf 最大的区别所在。你不需要手动编写 Dart 模型类,而是通过 .spy.yaml 文件定义数据结构,让 Serverpod 自动生成对应的 Dart 类。

一个 Company 的模型文件如下所示:

yaml
class: Company
table: company
fields:
  name: String
  foundedDate: DateTime?

运行 serverpod generate 会生成一个完整的 Dart 类,包含以下内容:

  • 不可变字段及其正确类型
  • 用于序列化的 toJsonfromJson 方法
  • 通过静态 db 访问器进行数据库绑定
  • 构造函数和 copyWith 方法
  • 客户端包中也会生成相同的类,因此 Flutter 应用可以直接使用

这是核心的生产力提升。你只需在 YAML 中定义一次数据结构,就能获得一个一致且类型安全的模型,它可以在服务器、数据库和客户端之间无缝工作,而无需重复。

内置 ORM

Serverpod 的 ORM 直接使用生成的模型类。所有的数据库操作都通过模型上的静态 db 访问器进行:

dart
// 插入一行
var company = Company(name: 'Serverpod Corp', foundedDate: DateTime.now());
company = await Company.db.insertRow(session, company);

// 根据 ID 查找
var found = await Company.db.findById(session, 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 会自动生成迁移:

bash
serverpod create-migration

这会在 migrations/ 目录下创建一个 SQL 迁移文件。启动服务器时应用该迁移:

bash
dart bin/main.dart --apply-migrations

Serverpod 会跟踪哪些迁移已经应用,并只运行新的迁移。迁移系统与模型系统完全集成,因此不会出现 Dart 类与数据库模式之间的偏差。

启动开发服务器

在编写任何代码之前,先运行开发环境。

启动 Docker 容器(PostgreSQL 和 Redis):

bash
cd user_profile_api_server
docker compose up --build --detach

启动带有迁移应用的服务器:

bash
dart bin/main.dart --apply-migrations

你应该会看到以下输出:

code
SERVERPOD version: 2.x.x, mode: development
Insights listening on port 8081
Server default listening on port 8080
Webserver listening on port 8082

共有三个端口:端口 8080 是主 API 服务器;端口 8081 是 Serverpod Insights 工具,用于监控;端口 8082 是一个可选的 Web 服务器。对于本文,我们将 exclusively 使用端口 8080。

定义模型

用户模型

在服务器包的 lib/src/models/ 目录下创建 user.spy.yaml 文件:

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 内部 auth 模块中的 User 类冲突。table 键定义了 PostgreSQL 表名。indexes 块在 email 列上创建了一个唯一索引,确保数据库层面的唯一性。

Serverpod 会自动为每个带有 table 键的模型添加一个类型为 int?id 字段。你无需自行声明。

配置文件模型

创建 lib/src/models/profile.spy.yaml 文件:

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 是一个引用 AppUserid 的整数。Serverpod 的模型系统目前尚未在 YAML 中支持外键声明语法,因此引用完整性由端点逻辑中的应用程序层处理。

运行代码生成

在两个模型文件就位后,运行生成器:

bash
serverpod generate

这会在 lib/src/generated/ 目录下生成 Dart 类。对于 AppUser,你会得到:

dart
// This is auto-generated, never edit directly
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;
markdown
  // ORM操作的数据库访问器
  static final db = AppUserRepository._();

  // 序列化方法
  factory AppUser.fromJson(Map<String, dynamic> jsonSerialization, ...) { ... }
  Map<String, dynamic> toJson() { ... }
}

生成的代码是您的端点所交互的内容。您永远不会手动编写此代码。

创建和应用迁移

有了生成的模型后,创建迁移:

code
serverpod create-migration

这会在 migrations/ 目录中创建带有时间戳的 SQL 文件。应用它们:

code
dart bin/main.dart --apply-migrations

现在,PostgreSQL 中存在 app_usersprofiles 表,并且具有正确的列和索引。

构建 API

认证端点

创建 lib/src/endpoints/auth_endpoint.dart

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<Map<String, dynamic>> register(
    Session session,
    String email,
    String password,
    String firstName,
    String lastName,
  ) async {
    if (email.isEmpty || password.isEmpty || firstName.isEmpty || lastName.isEmpty) {
      throw Exception('所有字段都是必需的');
    }

    if (password.length < 8) {
      throw Exception('密码必须至少为8个字符');
    }

    // 检查是否存在现有用户
    final existing = await AppUser.db.findFirstRow(
      session,
      where: (t) => t.email.equals(email),
    );

    if (existing != null) {
      throw Exception('该邮箱已存在账户');
    }

    final passwordHash = BCrypt.hashpw(password, BCrypt.gensalt());

    var user = AppUser(
      email: email,
      passwordHash: passwordHash,
      firstName: firstName,
      lastName: lastName,
    );

    user = await AppUser.db.insertRow(session, user);

    final token = _generateToken(user);

    return {
      'user': _sanitizeUser(user),
      'token': token,
    };
  }

  Future<Map<String, dynamic>> login(
    Session session,
    String email,
    String password,
  ) async {
    if (email.isEmpty || password.isEmpty) {
      throw Exception('邮箱和密码是必需的');
    }

    final user = await AppUser.db.findFirstRow(
      session,
      where: (t) => t.email.equals(email),
    );

    if (user == null || !BCrypt.checkpw(password, user.passwordHash)) {
      throw Exception('无效的邮箱或密码');
    }

    if (!user.isActive) {
      throw Exception('该账户已被停用');
    }

    final token = _generateToken(user);

    return {
      'user': _sanitizeUser(user),
      'token': token,
    };
  }

  String _generateToken(AppUser user) {
    final jwt = JWT({'sub': user.id, 'email': user.email});
    return jwt.sign(SecretKey(_jwtSecret), expiresIn: const Duration(hours: 24));
  }

  // 永远不要将密码哈希返回给客户端
  Map<String, dynamic> _sanitizeUser(AppUser user) => {
        'id': user.id,
        'email': user.email,
        'firstName': user.firstName,
        'lastName': user.lastName,
        'isActive': user.isActive,
      };

  // 从 Serverpod 的配置系统读取
  String get _jwtSecret =>
      Session.serverpod.getPassword('jwtSecret') ?? 'fallback_dev_secret';
}

Serverpod 端点返回类型化的值。当您返回 Map<String, dynamic> 时,Serverpod 会自动对其进行序列化。当您抛出 Exception 时,Serverpod 会捕获它并向客户端返回结构化的错误响应。无需手动处理响应格式,也无需管理常见情况下的状态码。

用户端点

创建 lib/src/endpoints/user_endpoint.dart

dart
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart';

class UserEndpoint extends Endpoint {
  @override
  bool get requireLogin => true;

  Future<List<Map<String, dynamic>>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<void> 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<String, dynamic> _sanitizeUser(AppUser user) => {
        'id': user.id,
        'email': user.email,
        'firstName': user.firstName,
        'lastName': user.lastName,
        'isActive': user.isActive,
      };
}

请注意 @override bool get requireLogin => true。这是 Serverpod 内置的保护端点机制。当此 getter 返回 true 时,Serverpod 在调用方法之前验证每个请求到此端点的认证令牌。框架会自动拒绝未认证的请求。

配置文件端点

创建 lib/src/endpoints/profile_endpoint.dart

dart
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart';
dart
class ProfileEndpoint extends Endpoint {
  @override
  bool get requireLogin => true;

  Future<Map<String, dynamic>> getByUserId(
    Session session,
    int userId,
  ) async {
    final user = await AppUser.db.findById(session, userId);
    if (user == null || !user.isActive) {
      throw Exception('User not found');
    }

    final profile = await Profile.db.findFirstRow(
      session,
      where: (t) => t.userId.equals(userId),
    );

    if (profile == null) {
      throw Exception('Profile not found');
    }

    return _profileToMap(profile);
  }

  Future<Map<String, dynamic>> 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('User not found');
    }

    final existing = await Profile.db.findFirstRow(
      session,
      where: (t) => t.userId.equals(userId),
    );

    if (existing != null) {
      throw Exception('Profile already exists for this user');
    }

    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<Map<String, dynamic>> 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('Profile not found');
    }

    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<String, dynamic> _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 注册它们:

code
serverpod generate

认证

密码哈希与 JWT

在服务器包的 pubspec.yaml 文件中添加所需的依赖项:

yaml
dependencies:
  serverpod: ^2.5.0
  bcrypt: ^1.1.3
  dart_jsonwebtoken: ^2.12.0

然后运行 dart pub get

认证端点中的 _generateToken_sanitizeUser 辅助函数处理密码哈希和 JWT 的生成。JWT 的密钥通过 Session.serverpod.getPassword('jwtSecret') 从 Serverpod 内置的密码管理系统中读取。

将密钥添加到 config/passwords.yaml 文件中:

yaml
development:
  database: 'dart_password'
  jwtSecret: 'your_development_jwt_secret_here'

该文件默认已包含在 .gitignore 中。生产环境的秘密可以通过环境变量或 Serverpod Cloud 的秘密管理系统注入。

保护端点

Serverpod 提供了两个级别的端点保护:

  • requireLogin — 自动拒绝未认证的请求:
dart
@override
bool get requireLogin => true;
  • requiredScopes — 需要特定权限范围:
dart
@override
Set<Scope> get requiredScopes => {Scope.admin};

对于本文中的用户和配置文件端点,requireLogin 已足够。登录响应中的令牌会在每次后续请求中通过 Authorization 头传递,Serverpod 在调用端点方法之前会验证它。

在端点内部验证令牌以获取当前用户的 ID:

dart
Future<void> someProtectedMethod(Session session) async {
  final authInfo = await session.authenticated;

  if (authInfo == null) {
    throw Exception('Not authenticated');
  }

  final userId = authInfo.userId;
  // 使用 userId 继续操作
}

Serverpod 中的错误处理

Serverpod 会捕获端点方法抛出的异常,并自动将其转换为结构化的错误响应。当你抛出以下异常时:

dart
throw Exception('User not found');

客户端将收到一个结构化的错误响应。为了更精细的控制,Serverpod 提供了类型化的异常:

dart
throw ServerpodClientException('User not found', statusCode: 404);

对于服务器端的日志记录而不向客户端暴露详细信息:

dart
session.log('Unexpected error during user creation', level: LogLevel.error);
throw Exception('An internal error occurred');

Serverpod 的日志系统将日志存储在数据库中,并通过端口 8081 上的 Insights 仪表板使其可查询。每个请求都会自动记录时间信息、端点名称和结果,无需额外的中间件。

测试 API

Serverpod 通过 HTTP 暴露其端点。你可以直接使用 curl 进行测试,但请求格式遵循 Serverpod 的 RPC 约定,而不是传统的 REST 结构。

生成的端点方法 URL 模式如下:

code
POST /[endpoint]/[method]

请求体为包含方法参数的 JSON 数据。

注册用户:

bash
curl http://localhost:8080/auth/register \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "email": "seyi@example.com",
    "password": "securepassword",
    "firstName": "Seyi",
    "lastName": "Dev"
  }'

登录:

bash
curl http://localhost:8080/auth/login \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"email": "seyi@example.com", "password": "securepassword"}'

获取所有用户(已认证):

bash
curl http://localhost:8080/user/getAll \
  -X POST \
  -H "Authorization: Bearer eyJhbGci..."

按 ID 获取用户:

bash

curl http://localhost:8080/user/getById \ -X POST \ -H "Authorization: Bearer eyJhbGci..." \ -H "Content-Type: application/json" \ -d '{"userId": 1}'

创建一个用户档案:

bash
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"
  }'

更新用户信息:

bash
curl http://localhost:8080/user/update \
  -X POST \
  -H "Authorization: Bearer eyJhbGci..." \
  -H "Content-Type: application/json" \
  -d '{"userId": 1, "firstName": "Oluwaseyi"}'

删除用户:

bash
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:

bash
fly auth login

步骤 2 —— 从服务器目录启动应用程序:

bash
cd user_profile_api_server
fly launch

步骤 3 —— 设置生产环境密钥:

bash
fly secrets set JWT_SECRET="your_production_jwt_secret"

步骤 4 —— 更新生产环境配置:

编辑 config/production.yaml,填写由 Fly 提供的数据库连接详情。Fly 会注入 DATABASE_URL 环境变量,你可以将其映射到 Serverpod 的配置格式。

步骤 5 —— 部署:

bash
fly deploy

步骤 6 —— 第一次部署时应用迁移:

bash
fly ssh console
dart bin/main.dart --apply-migrations --mode production

使用 Serverpod Cloud 部署

Serverpod Cloud 是专门为 Serverpod 应用程序构建的原生部署平台。它以最少的配置处理数据库配置、扩展、监控和部署。

安装 Serverpod Cloud CLI:

bash
dart pub global activate serverpod_cloud_cli

认证:

bash
scloud login

在 cloud.serverpod.dev 的 Serverpod Cloud 控制台中创建一个项目,然后链接你的本地项目:

bash
scloud link --project-id your-project-id

部署:

bash
scloud deploy

Serverpod Cloud 会自动配置一个托管的 PostgreSQL 数据库,应用你的迁移,并部署你的服务器。它还提供 Insights 仪表板,用于监控生产环境中的请求、日志和性能。

对于已经致力于 Serverpod 生态系统的团队来说,Serverpod Cloud 是最快进入生产环境的路径。

结论

Serverpod 从根本上不同于 Shelf。Shelf 给你控制权,而 Serverpod 给你速度。你只需在 YAML 中定义模型,运行生成器,即可自动生成数据库类、序列化代码和客户端代码。你编写一个端点方法,框架会处理路由、参数提取、认证和错误格式化。

ORM 是体验中最强大的部分。类型安全的查询表达式、自动生成的迁移以及代码与模式之间无 SQL drift(SQL漂移),使得数据库操作比原始 SQL 更快更安全。

代价是灵活性不足。Serverpod 的 URL 结构、序列化格式和架构约定并非可选。如果你的 API 需要符合特定的 REST 结构,而该结构与 Serverpod 的 RPC 风格不同,那么你将与框架背道而驰。

对于全新的 Flutter 后端,尤其是 Dart 客户端将消费 API 的场景,Serverpod 几乎无可匹敌。服务器与客户端之间的代码共享、自动生成客户端代码以及紧密的工具链集成,使其成为目前最高效的 Dart 后端选项。

对于需要服务多个客户端、遵循外部 REST 约定或与现有基础设施集成(这些基础设施不期望 Serverpod 格式)的 API,低级工具如 Shelf 会提供更多的控制。如果你想了解如何使用 Shelf 构建并发布相同的用户和档案管理 API,并直接比较两种方法,请查看 这篇文章

知道哪种工具适合哪种任务,正是区分一个知道框架的开发者和一个理解后端开发的开发者的关键。

祝编程愉快!



免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人获得开发者工作。开始学习

code

AI 可能会生成不准确的信息,请核实重要内容