Skip to main content

Command Palette

Search for a command to run...

Dart Frog Tutorial Part 2: Building Your First Real REST API (Full CRUD with Todos) 🐸

We'll create a real Todo REST API with full CRUD operations in Dart Frog using in-memory storage (clean repository pattern for best practices).

Published
•3 min read
Dart Frog Tutorial Part 2: Building Your First Real REST API (Full CRUD with Todos) 🐸
S

Hello, I'm Samuel, also known as Tech With Sam.

I am passionate about learning and teaching programming, particularly Flutter and Dart at the moment. Please support me by subscribing to my newsletter. Thanks!

Subscribe for weekly tutorials and tips, or DM me to bring your app idea to life.

Questions? Join me on Discord: https://discord.gg/8X7dPYujqm For Business: techwithsam10@gmail.com

Hey guys! Welcome to Part 2 of our Dart Frog series. If you missed Part 1, we set up Dart Frog and built a basic API with hot reload. Watch it now if you’re new!

Today, we’re leveling up: Building your first real REST API, a full CRUD Todo endpoint, clean, and production-ready backends in pure Dart.

We’ll use dynamic routes, UUIDs, validation, and proper errors. By the end, you’ll have a testable API ready for your Flutter app next video. Let’s jump in!

Planning & Best Practices

Quick plan: We’ll create a Todo model with id, title, and completed status. Store them in-memory (Map for fast lookup), perfect for learning, and easy to upgrade to Postgres or Drift later.

Best practices:

  • Dynamic routes with [id].dart

  • UUID package for unique IDs

  • Validate JSON bodies

  • Return correct status: 200, 201, 404, 400.

Open your project from Part 1 — or create a new one with dart_frog create todo_api. Run dart_frog dev.

First, add UUID: pubspec.yaml

dependencies:
  uuid: ^4.5.0

flutter pub get (or dart pub get).

Create model: lib/src/todo.dart

///
class Todo {
  ///
  Todo({required this.id, required this.title, this.isCompleted = false});

  /// fromJson
  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      id: json[’id’] as String,
      title: json[’title’] as String,
      isCompleted: json[’isCompleted’] as bool? ?? false,
    );
  }

  /// id
  final String id;

  /// title
  final String title;

  /// isCompleted
  bool isCompleted;

  /// toJson
  Map<String, dynamic> toJson() {
    return {
      ‘id’: id,
      ‘title’: title,
      ‘isCompleted’: isCompleted,
    };
  }
}

In-memory store: lib/src/todo_repository.dart

import ‘package:my_project/src/todo_model.dart’;
import ‘package:uuid/uuid.dart’;

const _uuid = Uuid();
final _todos = <String, Todo>{};

/// get all todos
List<Todo> getAllTodos() => _todos.values.toList();

/// get a tod
Todo? getTodoById(String id) => _todos[id];

/// create
void createTodo(String title) {
  final id = _uuid.v4();
  _todos[id] = Todo(id: id, title: title);
}

/// update
void updateTodo(String id, {String? title, bool? isCompleted}) {
  final todo = _todos[id];
  if (todo == null) return;
  _todos[id] = Todo(
    id: id,
    title: title ?? todo.title,
    isCompleted: isCompleted ?? todo.isCompleted,
  );
}

/// delete
void deleteTodo(String id) => _todos.remove(id);

Now routes!

Collection: routes/todos/index.dart

import ‘package:dart_frog/dart_frog.dart’;
import ‘package:my_project/src/todo_repository.dart’;

Future<Response> onRequest(RequestContext context) async {
  switch (context.request.method) {
    case HttpMethod.get:
      final todos = getAllTodos();
      return Response.json(body: todos.map((e) => e.toJson()).toList());
    case HttpMethod.post:
      final body = await context.request.json() as Map<String, dynamic>;
      final title = body[’title’] as String?;
      if (title == null || title.isEmpty) {
        return Response(statusCode: 400, body: ‘Title is required’);
      }
      createTodo(title);
      return Response(statusCode: 201, body: ‘Todo created’);
    case HttpMethod.delete:
    case HttpMethod.put:
    case HttpMethod.patch:
    case HttpMethod.head:
    case HttpMethod.options:
      return Response(statusCode: 405);
  }
}

Dynamic item: routes/todos/[id].dart

import ‘package:dart_frog/dart_frog.dart’;
import ‘package:my_project/src/todo_repository.dart’;

Future<Response> onRequest(RequestContext context, String id) async {
  final todo = getTodoById(id);
  if (todo == null) return Response(statusCode: 404);

  switch (context.request.method) {
    case HttpMethod.get:
      return Response.json(body: todo.toJson());
    case HttpMethod.put:
      final body = await context.request.json() as Map<String, dynamic>;
      final title = body[’title’] as String?;
      final isCompleted = body[’isCompleted’] as bool?;
      updateTodo(id, title: title, isCompleted: isCompleted);
      return Response.json(body: getTodoById(id)!.toJson());
    case HttpMethod.delete:
      deleteTodo(id);
      return Response(statusCode: 204);
    case HttpMethod.post:
    case HttpMethod.patch:
    case HttpMethod.head:
    case HttpMethod.options:
      return Response(statusCode: 405);
  }
}

Testing + Wrap (Show curl/Postman)

Quick tests:

curl http://localhost:8080/todos
curl -X POST http://localhost:8080/todos -H “Content-Type: application/json” -d ‘{”title”: “Learn Dart Frog”}’
curl http://localhost:8080/todos/<generated-id>

Handles errors gracefully. Production-ready foundation!

Source Code 👇 — Show some ❤️ by starring ⭐ the repo and follow me 😄! https://github.com/techwithsam/dart_frog_full_course_tutorial

This is your first real Dart Frog REST API — congrats! Next: Connect a Flutter app to it.

Samuel Adekunle, Tech With Sam YouTube.