
Building fullstack applications in Flutter has become remarkably streamlined with Serverpod. This Dart-based backend framework allows Flutter developers to write both client and server code in the same language, with type-safe API generation, shared models, and built-in features like authentication, file storage, and real-time communication. In this comprehensive guide, you’ll learn how to set up Serverpod, create endpoints with database integration, build shared models, and connect everything to a Flutter client.
What Is Serverpod?
Serverpod is a Dart-based backend framework designed specifically for Flutter developers. It provides:
- Shared Models – Define once, use on client and server
- Auto-Generated APIs – Type-safe client code generated automatically
- Database ORM – Native PostgreSQL integration with migrations
- Authentication – Built-in email, Google, Apple sign-in
- File Storage – Local or cloud file handling
- Real-Time – WebSocket support for live updates
- Caching – Redis integration for performance
Serverpod vs Serverpod Mini
| Feature | Serverpod | Serverpod Mini |
|---|---|---|
| Database | PostgreSQL | None (add manually) |
| Docker Required | Yes | No |
| ORM | Full ORM | None |
| Caching | Redis | None |
| Use Case | Production apps | Prototypes, simple APIs |
Project Setup
Prerequisites
# Install Flutter
flutter --version
# Install Serverpod CLI
dart pub global activate serverpod_cli
# Add to PATH
export PATH="$PATH":"$HOME/.pub-cache/bin"
# Verify installation
serverpod --version
# Install Docker Desktop (for full Serverpod)
# https://docs.docker.com/get-docker/
Create a New Project
# Full Serverpod project
serverpod create my_app
# Or Mini version (no database)
serverpod create my_app --mini
This generates three packages:
my_app/
├── my_app_server/ # Server-side Dart code
├── my_app_client/ # Auto-generated client library
└── my_app_flutter/ # Flutter app
Start the Server
# Start Docker containers (PostgreSQL, Redis)
cd my_app/my_app_server
docker compose up --build --detach
# Run the server with migrations
dart bin/main.dart --apply-migrations
# Server runs on:
# - 8080: Main API
# - 8081: Insights (admin)
# - 8082: Web server
Defining Models
Models are defined in YAML files in the lib/src/protocol/ directory:
# lib/src/protocol/task.yaml
class: Task
table: tasks
fields:
title: String
description: String?
completed: bool
priority: Priority
dueDate: DateTime?
createdAt: DateTime
updatedAt: DateTime?
userId: int
indexes:
task_user_idx:
fields: userId
task_completed_idx:
fields: completed
# lib/src/protocol/priority.yaml
enum: Priority
values:
- low
- medium
- high
- urgent
# lib/src/protocol/user.yaml
class: User
table: users
fields:
email: String
name: String
avatarUrl: String?
createdAt: DateTime
indexes:
user_email_idx:
fields: email
unique: true
Generate Code
# Generate Dart classes, client code, and migrations
serverpod generate
# Apply database migrations
dart bin/main.dart --apply-migrations
Creating Endpoints
// lib/src/endpoints/task_endpoint.dart
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart';
class TaskEndpoint extends Endpoint {
/// Get all tasks for the authenticated user
Future<List<Task>> getTasks(Session session) async {
final userId = await _requireAuth(session);
return Task.db.find(
session,
where: (t) => t.userId.equals(userId),
orderBy: (t) => t.createdAt,
orderDescending: true,
);
}
/// Get a single task by ID
Future<Task?> getTask(Session session, int id) async {
final userId = await _requireAuth(session);
final task = await Task.db.findById(session, id);
if (task == null || task.userId != userId) {
return null;
}
return task;
}
/// Create a new task
Future<Task> createTask(
Session session,
String title,
String? description,
Priority priority,
DateTime? dueDate,
) async {
final userId = await _requireAuth(session);
final task = Task(
title: title,
description: description,
completed: false,
priority: priority,
dueDate: dueDate,
createdAt: DateTime.now(),
userId: userId,
);
return await Task.db.insertRow(session, task);
}
/// Update an existing task
Future<Task?> updateTask(
Session session,
int id,
String? title,
String? description,
bool? completed,
Priority? priority,
DateTime? dueDate,
) async {
final userId = await _requireAuth(session);
final task = await Task.db.findById(session, id);
if (task == null || task.userId != userId) {
return null;
}
final updatedTask = task.copyWith(
title: title ?? task.title,
description: description ?? task.description,
completed: completed ?? task.completed,
priority: priority ?? task.priority,
dueDate: dueDate ?? task.dueDate,
updatedAt: DateTime.now(),
);
return await Task.db.updateRow(session, updatedTask);
}
/// Delete a task
Future<bool> deleteTask(Session session, int id) async {
final userId = await _requireAuth(session);
final task = await Task.db.findById(session, id);
if (task == null || task.userId != userId) {
return false;
}
return await Task.db.deleteRow(session, task);
}
/// Get tasks filtered by completion status
Future<List<Task>> getTasksByStatus(
Session session,
bool completed,
) async {
final userId = await _requireAuth(session);
return Task.db.find(
session,
where: (t) => t.userId.equals(userId) & t.completed.equals(completed),
orderBy: (t) => t.priority,
);
}
/// Helper to require authentication
Future<int> _requireAuth(Session session) async {
final authInfo = await session.authenticated;
if (authInfo == null) {
throw AuthenticationRequiredException();
}
return authInfo.userId;
}
}
User Endpoint
// lib/src/endpoints/user_endpoint.dart
import 'package:serverpod/serverpod.dart';
import '../generated/protocol.dart';
class UserEndpoint extends Endpoint {
/// Get current user profile
Future<User?> getCurrentUser(Session session) async {
final authInfo = await session.authenticated;
if (authInfo == null) return null;
return User.db.findById(session, authInfo.userId);
}
/// Update user profile
Future<User?> updateProfile(
Session session,
String? name,
String? avatarUrl,
) async {
final authInfo = await session.authenticated;
if (authInfo == null) return null;
final user = await User.db.findById(session, authInfo.userId);
if (user == null) return null;
final updatedUser = user.copyWith(
name: name ?? user.name,
avatarUrl: avatarUrl ?? user.avatarUrl,
);
return await User.db.updateRow(session, updatedUser);
}
/// Get task statistics for user
Future<TaskStats> getStats(Session session) async {
final authInfo = await session.authenticated;
if (authInfo == null) {
throw AuthenticationRequiredException();
}
final total = await Task.db.count(
session,
where: (t) => t.userId.equals(authInfo.userId),
);
final completed = await Task.db.count(
session,
where: (t) => t.userId.equals(authInfo.userId) & t.completed.equals(true),
);
return TaskStats(
totalTasks: total,
completedTasks: completed,
pendingTasks: total - completed,
);
}
}
Flutter Client Integration
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:my_app_client/my_app_client.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';
late Client client;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
client = Client(
'http://localhost:8080/',
authenticationKeyManager: FlutterAuthenticationKeyManager(),
)
..connectivityMonitor = FlutterConnectivityMonitor();
runApp(const MyApp());
}
// lib/services/task_service.dart
import '../main.dart';
import 'package:my_app_client/my_app_client.dart';
class TaskService {
Future<List<Task>> getTasks() async {
try {
return await client.task.getTasks();
} catch (e) {
throw Exception('Failed to load tasks: $e');
}
}
Future<Task> createTask({
required String title,
String? description,
required Priority priority,
DateTime? dueDate,
}) async {
return await client.task.createTask(
title,
description,
priority,
dueDate,
);
}
Future<Task?> updateTask({
required int id,
String? title,
String? description,
bool? completed,
Priority? priority,
DateTime? dueDate,
}) async {
return await client.task.updateTask(
id,
title,
description,
completed,
priority,
dueDate,
);
}
Future<bool> deleteTask(int id) async {
return await client.task.deleteTask(id);
}
Future<List<Task>> getPendingTasks() async {
return await client.task.getTasksByStatus(false);
}
Future<List<Task>> getCompletedTasks() async {
return await client.task.getTasksByStatus(true);
}
}
Task List Screen
// lib/screens/task_list_screen.dart
import 'package:flutter/material.dart';
import 'package:my_app_client/my_app_client.dart';
import '../services/task_service.dart';
class TaskListScreen extends StatefulWidget {
const TaskListScreen({super.key});
@override
State<TaskListScreen> createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State<TaskListScreen> {
final _taskService = TaskService();
List<Task> _tasks = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadTasks();
}
Future<void> _loadTasks() async {
setState(() => _isLoading = true);
try {
_tasks = await _taskService.getTasks();
_error = null;
} catch (e) {
_error = e.toString();
}
setState(() => _isLoading = false);
}
Future<void> _toggleComplete(Task task) async {
final updated = await _taskService.updateTask(
id: task.id!,
completed: !task.completed,
);
if (updated != null) {
_loadTasks();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Tasks'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadTasks,
),
],
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: () => _showCreateDialog(context),
child: const Icon(Icons.add),
),
);
}
Widget _buildBody() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $_error'),
ElevatedButton(onPressed: _loadTasks, child: const Text('Retry')),
],
),
);
}
if (_tasks.isEmpty) {
return const Center(child: Text('No tasks yet. Create one!'));
}
return RefreshIndicator(
onRefresh: _loadTasks,
child: ListView.builder(
itemCount: _tasks.length,
itemBuilder: (context, index) => _buildTaskTile(_tasks[index]),
),
);
}
Widget _buildTaskTile(Task task) {
return ListTile(
leading: Checkbox(
value: task.completed,
onChanged: (_) => _toggleComplete(task),
),
title: Text(
task.title,
style: TextStyle(
decoration: task.completed ? TextDecoration.lineThrough : null,
),
),
subtitle: Text(task.priority.name.toUpperCase()),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
await _taskService.deleteTask(task.id!);
_loadTasks();
},
),
);
}
void _showCreateDialog(BuildContext context) {
// Show dialog to create new task
}
}
Common Mistakes to Avoid
1. Forgetting to Generate Code
# Wrong - code not regenerated after model changes
# Client will have outdated types
# Correct - always regenerate after changes
serverpod generate
2. Not Applying Migrations
# Wrong - database schema out of sync
dart bin/main.dart
# Correct - apply migrations
dart bin/main.dart --apply-migrations
3. Exposing Sensitive Fields
# Wrong - password in model
class: User
fields:
email: String
passwordHash: String # Exposed to client!
# Correct - use serverOnly scope
class: User
fields:
email: String
passwordHash: String, scope=serverOnly
4. Missing Authentication Checks
// Wrong - no auth check
Future<List<Task>> getTasks(Session session) async {
return Task.db.find(session); // Returns ALL tasks!
}
// Correct - verify user
Future<List<Task>> getTasks(Session session) async {
final auth = await session.authenticated;
if (auth == null) throw AuthenticationRequiredException();
return Task.db.find(session, where: (t) => t.userId.equals(auth.userId));
}
Final Thoughts
Serverpod empowers Flutter developers to build fullstack applications entirely in Dart. The type-safe API generation, shared models, and built-in features like authentication and file storage significantly reduce boilerplate and potential bugs. Start with Serverpod Mini for prototypes, then upgrade to the full version when you need database persistence. The investment in learning Serverpod pays off with faster development cycles and a more cohesive codebase.
For more Flutter development patterns, read Flutter Login/Register Flow with Riverpod and Flutter Build Once Run Anywhere. For official documentation and advanced features, visit the Serverpod Documentation.
1 Comment