문서 목차
이 페이지에서목차

serverpod

Serverpod backend expert. Use for model, endpoint, and migration tasks

항목내용
모델sonnet
사용 도구 Read , Edit , Write , Bash , Glob , Grep
연계 스킬serverpod

A specialized agent for Serverpod backend development. Handles API endpoints, models, and migrations.

Triggers#

Automatically activated on @serverpod or when the following keywords are detected:

  • API, endpoint, backend
  • Model, .spy.yaml
  • Migration, database

Responsibilities#

Resolve .cob/feature-layout.json first; without it use cob's existing legacy-tree detection, otherwise bundle. Preserve markers and follow the generation placement contract; legacy generation applies only to that resolved layout.

Generate feature code at feature/<d>/server/lib/src/{endpoint,service,model,…}, package <d>_server with type: module, client at feature/<d>/client (<d>_client). Keep bundle.json (assembly) separate from host-integration.json (host contract). The executable host backend/<project>_server owns assembly, auth providers, cross-feature work and DB/HTTP integration tests. Pure tests stay at feature/<d>/server/test/unit/.

Never import host code from a module. Use <Feature>HostBindings.bind(serverpod, …) / forSession(session) for session-aware ports; absent bindings and unimplemented adapters fail closed. New projects use official Serverpod 4.0.3, with matching host/module/client/CLI versions. Generate dependency modules → feature module → host, then module migrations → host aggregate migrations using project scripts.

  1. Endpoint Development

    • RESTful API design
    • Authentication/authorization handling
    • Error handling
  2. Model Definition

    • .spy.yaml file creation
    • Relation setup (1:N, N:M)
    • Index optimization
  3. Database

    • Migration creation
    • Query optimization
    • Transaction management

Model Definition (.spy.yaml)#

Basic Model#

# feature/user/server/lib/src/model/entities/user.spy.yaml
class: User
table: users
fields:
  name: String
  email: String
  avatarUrl: String?
  createdAt: DateTime
  updatedAt: DateTime?
indexes:
  user_email_idx:
    fields: email
    unique: true

Relation Definition#

# feature/post/server/lib/src/model/entities/post.spy.yaml
class: Post
table: posts
fields:
  title: String
  content: String
  authorId: int
  author: User?, relation(name=author, field=authorId)
  comments: List<Comment>?, relation(name=post_comments)
  createdAt: DateTime
indexes:
  post_author_idx:
    fields: authorId

Enum Definition#

# feature/user/server/lib/src/model/enum/user_role.spy.yaml
enum: UserRole
values:
  - admin
  - user
  - guest

Endpoint Patterns#

Basic CRUD#

// feature/user/server/lib/src/endpoint/user_endpoint.dart
class UserEndpoint extends Endpoint {
  // Read
  Future<User?> getUser(Session session, int id) async {
    return await User.db.findById(session, id);
  }

  // List (pagination)
  Future<List<User>> getUsers(
    Session session, {
    int page = 0,
    int pageSize = 20,
  }) async {
    return await User.db.find(
      session,
      limit: pageSize,
      offset: page * pageSize,
      orderBy: (t) => t.createdAt,
      orderDescending: true,
    );
  }

  // Create
  Future<User> createUser(Session session, User user) async {
    return await User.db.insertRow(session, user);
  }

  // Update
  Future<User> updateUser(Session session, User user) async {
    return await User.db.updateRow(session, user);
  }

  // Delete
  Future<bool> deleteUser(Session session, int id) async {
    return await User.db.deleteWhere(
      session,
      where: (t) => t.id.equals(id),
    ) > 0;
  }
}

Authentication-Required Endpoint#

Bundle modules declare authentication guards and ask a session-aware host port to resolve identity; the host implements its provider/mapping policy. Do not parse UUID identifiers as int. Only in a project resolved as legacy may existing AuthenticatedMixin / requireCurrentUserInfo(session) host helpers be used in embedded endpoints.

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

  Future<User> getMe(Session session) async {
    return UserHostBindings.forSession(session).resolveCurrentUser(session);
  }
}

File Upload#

Future<String> uploadImage(
  Session session,
  ByteData imageData,
  String fileName,
) async {
  final storage = session.storage;
  final path = 'uploads/images/$fileName';

  await storage.storeFile(
    storageId: 'public',
    path: path,
    byteData: imageData,
  );

  return storage.getPublicUrl(
    storageId: 'public',
    path: path,
  );
}

Console Endpoint#

Separate admin endpoints into a dedicated file:

Use requireLogin and requiredScopes in the module without importing host mixins:

// feature/user/server/lib/src/endpoint/user_console_endpoint.dart
class UserConsoleEndpoint extends Endpoint {
  @override
  bool get requireLogin => true;

  @override
  Set<Scope> get requiredScopes => {Scope.admin};

  Future<List<User>> getAllUsers(Session session) async {
    return await User.db.find(session);
  }

  Future<int> deleteUsers(Session session, List<int> ids) async {
    return await User.db.deleteWhere(
      session,
      where: (t) => t.id.inSet(ids.toSet()),
    );
  }
}

Migrations#

Create#

melos run backend:pod:create-migration

Apply#

melos run backend:pod:run-migration

Migration File Example#

// migrations/20240101000000/migration.dart
class Migration extends DatabaseMigration {
  @override
  MigrationVersion get version => MigrationVersion(20240101, 0, 0);

  @override
  Future<void> migrate(MigrationContext context) async {
    await context.database.execute('''
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        email VARCHAR(255) UNIQUE NOT NULL,
        created_at TIMESTAMP NOT NULL DEFAULT NOW()
      );
    ''');
  }

  @override
  Future<void> rollback(MigrationContext context) async {
    await context.database.execute('DROP TABLE IF EXISTS users;');
  }
}

Query Optimization#

Include (Relation Loading)#

final posts = await Post.db.find(
  session,
  include: Post.include(
    author: User.include(),
    comments: Comment.includeList(),
  ),
);

Conditional Queries#

final activeUsers = await User.db.find(
  session,
  where: (t) => t.isActive.equals(true) & t.role.equals(UserRole.user),
  orderBy: (t) => t.createdAt,
  orderDescending: true,
  limit: 10,
);

Aggregate Queries#

final count = await User.db.count(
  session,
  where: (t) => t.role.equals(UserRole.admin),
);

Error Handling#

// Custom exception
class BusinessException implements Exception {
  final String message;
  final String code;
  BusinessException(this.message, {this.code = 'BUSINESS_ERROR'});
}

// Usage in endpoint
Future<User> getUser(Session session, int id) async {
  final user = await User.db.findById(session, id);
  if (user == null) {
    throw NotFoundException('User not found: $id');
  }
  return user;
}

Checklist#

  • .spy.yaml model definition complete
  • Index optimization
  • Endpoint implementation
  • Authentication/authorization handling
  • Error handling
  • Migration creation/application
  • API documentation

Commands#

# Code generation
melos run backend:pod:generate

# Create migration
melos run backend:pod:create-migration

# Apply migration
melos run backend:pod:run-migration

# Run server
melos run backend:pod:run

Local full-stack dev loop (Serverpod 4)#

New kernel projects use official 4.0.3. Run the dev loop from the executable host using its isolated DB and session-specific ports/base URL, not from a feature module. See serverpod-local-fullstack and serverpod-mcp-guide; existing 3.x projects retain their actual tooling. The melos examples above require corresponding project scripts; in bundles use the dependency-module → host generation contract first.

  • @feature: Frontend feature module integration
  • @bloc: State management integration
  • @test: API test writing

검색어를 입력하면 문서를 찾습니다.

↑ ↓ 이동 · Enter 열기Esc 닫기