DEV Community

Samuel Adekunle
Samuel Adekunle

Posted on • Originally published at techwithsam.dev

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

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 
Enter fullscreen mode Exit fullscreen mode

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,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)