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).

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.





