DEV Community

Cover image for Full-Stack Mobile Development (Flutter + Serverpod) #4 - Task CRUD Operations
Samuel Adekunle
Samuel Adekunle

Posted on

Full-Stack Mobile Development (Flutter + Serverpod) #4 - Task CRUD Operations

Hey, Flutter fam! Episode 4 of our Flutter x Serverpod series is here! Last time we integrated authentication: login, registration, reset/forgot password, validation code, and secure Home screen. If you're signed in and staring at an empty task list right now… perfect. Today, we're filling it with secure CRUD operations for our fintech to-do app.

We're creating, reading, updating, and deleting trade alerts that are fully owned by the logged-in user, with validation to prevent anyone from entering a negative amount. By the end, your app will feel production-ready, complete with empty states, loading spinners, error toasts, and more. Let's build! Subscribe and let's code! 🚀

FOR BETTER UNDERSTANDING, REFER TO THE YOUTUBE VIDEO.

First, our Task model 

In fintech_todo_server/lib/src/task.spy.yaml:

class: Task

table: task

fields:
   id: int?
   title: String
   description: String
   amount: double
   dueDate: DateTime?
   userId: int
Enter fullscreen mode Exit fullscreen mode

Create migration:

cd fintech_todo_server
serverpod create-migration
Enter fullscreen mode Exit fullscreen mode

Apply

dart run bin/main.dart --role maintenance --apply-migrations
Enter fullscreen mode Exit fullscreen mode

Done - Postgres now has a task table linked to the auth user table.

Building the Secure TaskEndpoint

File: fintech_todo_server/lib/src/task_endpoint.dart

import 'package:fintech_todo_server/src/exceptions.dart';
import 'package:fintech_todo_server/src/generated/protocol.dart';
import 'package:serverpod/serverpod.dart';

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

  // CREATE
  Future<Task> createTask(Session session, Task request) async {
    final auth = await session.authenticated;

    if (auth == null) {
      throw AuthorizationException(message: "You're not authenticated!");
    }
    final userId = auth.userId;

    // Validation
    if (request.title.isEmpty) {
      throw ValidationException(message: "Title cannot be empty");
    }

    if (request.amount <= 0) {
      throw ValidationException(message: "Amount must be greater than zero");
    }

    final task = Task(
      userId: userId,
      title: request.title,
      amount: request.amount,
      description: request.description,
      dueDate: request.dueDate,
    );

    final inserted = await Task.db.insertRow(session, task);
    return inserted;
  }

  // READ
  Future<List<Task>> getTasks(Session session) async {
    final auth = await session.authenticated;

    if (auth == null) {
      throw AuthorizationException(message: "You're not authenticated!");
    }
    final userId = auth.userId;

    final tasks = await Task.db.find(
      session,
      where: (t) => t.userId.equals(userId),
      orderBy: (t) => t.dueDate,
      orderDescending: true,
    );
    return tasks;
  }

  // UPDATE
  Future<Task> updateTask(Session session, Task request) async {
    final auth = await session.authenticated;

    if (auth == null) {
      throw AuthorizationException(message: "You're not authenticated!");
    }
    final userId = auth.userId;

    if (request.id == null) {
      throw ValidationException(message: "Task ID is required for update");
    }

    if (request.title.isEmpty) {
      throw ValidationException(message: "Title cannot be empty");
    }

    if (request.amount <= 0) {
      throw ValidationException(message: "Amount must be greater than zero");
    }

    final existingTask = await Task.db.findById(session, request.id!);

    if (existingTask == null) {
      throw NotFoundException(message: "Task with ID ${request.id} not found.");
    }
    if (existingTask.userId != userId) {
      throw AuthorizationException(
          message: "You do not have permission to update this task.");
    }

    final updatedTask = existingTask.copyWith(
      title: request.title,
      amount: request.amount,
      description: request.description,
      dueDate: request.dueDate,
    );

    final result = await Task.db.updateRow(session, updatedTask);
    return result;
  }

  // DELETE
  Future<void> deleteTask(Session session, int taskId) async {
    final auth = await session.authenticated;

    if (auth == null) {
      throw AuthorizationException(message: "You're not authenticated!");
    }
    final userId = auth.userId;

    final rowsDeleted = await Task.db.deleteWhere(
      session,
      where: (t) => t.id.equals(taskId) & t.userId.equals(userId),
    );

    if (rowsDeleted.isEmpty) {
      throw NotFoundException(message: "Task with ID $taskId not found.");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Regenerate client:

serverpod generate
Enter fullscreen mode Exit fullscreen mode

Flutter Side: Beautiful Task List + Forms

Switch to fintech_todo_flutter

Key screens we built live:
TaskListScreen - Empty state + ListView.builder
TaskFormDialog - Create/Edit modal
Delete confirmation

Code highlights (copy-paste ready in description):

// In HomeScreen
Future<void> _loadTasks() async {
  try {
    final tasks = await client.task.getTasks();
    setState(() => taskList = tasks);
  } catch (e) {
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Load failed: $e')));
  }
}
Enter fullscreen mode Exit fullscreen mode

// FAB → showDialog(TaskFormDialog())
Real demo flow:
Login → Empty state ("No trades yet - add one!")
Tap FAB → Fill form → Save → Task appears instantly
Edit → Update → List refreshes
Delete → Confirmation → Gone 

That's it - you now have a fully secure, user-owned task system! No more public todos - every task belongs to the signed-in trader.
I hope you have learn something incredible. Press that follow button if you're not following me yet. Also, make sure to subscribe to the newsletter so you're notified when I publish a new article. Kindly press the clap button as many times as you want if you enjoy it, and feel free to ask a question.

Source Code 👇 - Show some ❤️ by starring ⭐ the repo and follow me 😄!

GitHub logo techwithsam / fintech_todo_serverpod

Full-Stack Mobile Development (Flutter + Serverpod) - TechWithSam

Full-Stack with Flutter x Serverpod | Tech With Sam

Youtube Serverpod Flutter GitHub stars GitHub TechWithSam

Overview

Top question on Stack Overflow this year: 'How do I build a backend without leaving Dart?' Enter Serverpod—the open-source powerhouse that's making full-stack Dart the 2025 must-have. In this new series, we're building a real-world fintech to-do app from scratch: secure tasks, real-time updates, and cloud-ready deploys. No more half-solutions!

Youtube Banner

[Course] Full-Stack Mobile Development With Flutter and Serverpod - Watch on youtube


Project layout

  • fintech_todo_server/ — Serverpod server
    • bin/main.dart — server entrypoint
    • Dockerfile — for containerized deploys
    • migrations/ — DB migrations
    • lib/src/generated/ — generated protocol & endpoints
  • fintech_todo_flutter/ — Flutter client
    • lib/main.dart — app entrypoint
  • fintech_todo_client/ — generated client package

Quick start

Prereqs: Docker, Flutter, Dart SDK (for server).

  1. Clone:

    git clone https://github.com/techwithsam/fintech_todo_serverpod
    cd fintech_todo
    Enter fullscreen mode Exit fullscreen mode
  2. Start local DB & Redis (example using docker-compose):

    cd fintech_todo_server && docker compose up -d
    Enter fullscreen mode Exit fullscreen mode
  3. Run the server:

    # from fintech_todo/
    dart pub get
    dart
    Enter fullscreen mode Exit fullscreen mode

Drop a like if this helped, comment your experience with Serverpod and this series, and join Discord for the updated repo + bonus snippets: https://discord.gg/NytgTkyw3R

Samuel Adekunle, Tech With Sam YouTube | Part 5Teaser

Happy Building! 🥰👨‍💻

Top comments (0)