Serverpod vs Dart Frog

Serverpod
Serverpod
Published in
4 min readAug 2, 2024

We were curious to see how Serverpod stacked up against Dart Frog, another popular Dart backend framework. To this end, we replicated the Todos example from Frog’s website.

Serverpod vs Dart Frog

Data models

Dart Frog does not provide a native way of serializing data. Instead, it relies on third-party packages such as json_serializable to read and write the models. This can be useful if you want to include logic with your models, but it won’t work across computer languages. This is the model from the Todos example.

// Dart Frog - Data model

import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';

part 'todo.g.dart';

@immutable
@JsonSerializable()
class Todo extends Equatable {
Todo({
this.id,
required this.title,
this.description = '',
this.isCompleted = false,
}) : assert(id == null || id.isNotEmpty, 'id cannot be empty');

final String? id;

final String title;

final String description;

final bool isCompleted;

Todo copyWith({
String? id,
String? title,
String? description,
bool? isCompleted,
}) {
return Todo(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
isCompleted: isCompleted ?? this.isCompleted,
);
}

static Todo fromJson(Map<String, dynamic> json) => _\$TodoFromJson(json);

Map<String, dynamic> toJson() => _\$TodoToJson(this);

@override
List<Object?> get props => [id, title, description, isCompleted];
}

In contrast, Serverpod defines models using easy-to-read YAML files. Dart code is generated from the models, which include serialization methods and a copyWith method. Another benefit for Serverpod is that the models can also be mapped to the database through Serverpod’s ORM. (If you really want it, Serverpod supports custom serialization, too.)

# Serverpod - Data model

class: Todo

fields:
id: UuidValue?
title: String
description: String?
isCompleted: bool

Endpoints

Next, let’s look at how you build your endpoints and methods with Dart Frog and Serverpod. The frameworks take very different approaches. Dart Frog requires you to handle all serialization and HTTP requests manually, whereas Serverpod uses remote method calls.

Here is the code from Dart Frog’s Todos example. First, we need to create our middleware.

// Dart Frog - Create middleware

import 'package:dart_frog/dart_frog.dart';
import 'package:in_memory_todos_data_source/in_memory_todos_data_source.dart';

final _dataSource = InMemoryTodosDataSource();

Handler middleware(Handler handler) {
return handler
.use(requestLogger())
.use(provider<TodosDataSource>((_) => _dataSource));
}

Next, we set up a /todos route.

// Dart Frog - /todos route

import 'dart:async';
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:todos_data_source/todos_data_source.dart';

FutureOr<Response> onRequest(RequestContext context) async {
switch (context.request.method) {
case HttpMethod.get:
return _get(context);
case HttpMethod.post:
return _post(context);
case HttpMethod.delete:
case HttpMethod.head:
case HttpMethod.options:
case HttpMethod.patch:
case HttpMethod.put:
return Response(statusCode: HttpStatus.methodNotAllowed);
}
}

Future<Response> _get(RequestContext context) async {
final dataSource = context.read<TodosDataSource>();
final todos = await dataSource.readAll();
return Response.json(body: todos);
}

Future<Response> _post(RequestContext context) async {
final dataSource = context.read<TodosDataSource>();
final todo = Todo.fromJson(
await context.request.json() as Map<String, dynamic>,
);

return Response.json(
statusCode: HttpStatus.created,
body: await dataSource.create(todo),
);
}

Finally, we have a /todos/<id> route.

// Dart Frog - /todos/<id> route

import 'dart:async';
import 'dart:io';

import 'package:dart_frog/dart_frog.dart';
import 'package:todos_data_source/todos_data_source.dart';

FutureOr<Response> onRequest(RequestContext context, String id) async {
final dataSource = context.read<TodosDataSource>();
final todo = await dataSource.read(id);

if (todo == null) {
return Response(statusCode: HttpStatus.notFound, body: 'Not found');
}

switch (context.request.method) {
case HttpMethod.get:
return _get(context, todo);
case HttpMethod.put:
return _put(context, id, todo);
case HttpMethod.delete:
return _delete(context, id);
case HttpMethod.head:
case HttpMethod.options:
case HttpMethod.patch:
case HttpMethod.post:
return Response(statusCode: HttpStatus.methodNotAllowed);
}
}

Future<Response> _get(RequestContext context, Todo todo) async {
return Response.json(body: todo);
}

Future<Response> _put(RequestContext context, String id, Todo todo) async {
final dataSource = context.read<TodosDataSource>();
final updatedTodo = Todo.fromJson(
await context.request.json() as Map<String, dynamic>,
);
final newTodo = await dataSource.update(
id,
todo.copyWith(
title: updatedTodo.title,
description: updatedTodo.description,
isCompleted: updatedTodo.isCompleted,
),
);

return Response.json(body: newTodo);
}

Future<Response> _delete(RequestContext context, String id) async {
final dataSource = context.read<TodosDataSource>();
await dataSource.delete(id);
return Response(statusCode: HttpStatus.noContent);
}

So, how do we do all this with Serverpod? In Serverpod, you add your endpoint methods to an Endpoint. Serverpod will analyze your server code and automatically create all the required server-side code. We use the same InMemoryTodosDataSource in this example as in the Dart Frog example.

import 'package:serverpod/serverpod.dart';
import 'package:todo_server/src/business/in_memory_data_source.dart';
import 'package:todo_server/src/generated/protocol.dart';

final _dataSource = InMemoryTodosDataSource();

class TodoEndpoint extends Endpoint {
Future<Todo> create(Session session, Todo todo) async {
return _dataSource.create(todo);
}

Future<List<Todo>> getAll(Session session) async {
return _dataSource.readAll();
}

Future<Todo?> get(Session session, UuidValue id) async {
return _dataSource.read(id);
}

Future<Todo> update(Session session, Todo todo) async {
return _dataSource.update(todo.id!, todo);
}

Future<void> delete(Session session, UuidValue id) async {
return _dataSource.delete(id);
}
}

An added benefit of using Serverpod is that everything is strictly typed. Plus, Serverpod already knows how to serialize common types such as dates, UUID, and binary data, so you don’t need to figure that out yourself.

Calling the server from Flutter

Dart Frog offers little help when calling your server from Flutter. To do this, you must make the HTTP calls and serialize your data yourself.

// Dart Frog - Client code

var todo = Todo(
title: 'My Todo',
isCompleted: false,
);

var url = Uri.parse('http://localhost:8080/todos');
var headers = {'Content-Type': 'application/json'};
var body = jsonEncode(todo.toJson());

var response = await http.post(
url,
headers: headers,
body: body,
);

var newTodo = Todo.fromJson(jsonDecode(response.body));

In contrast, Serverpod will automatically create a client package for you. So, calling your endpoint method will be as easy as calling a local method. The models you create are also accessible from your Flutter app.

// Serverpod - Client code

var todo = Todo(
title: 'My Todo',
isCompleted: false,
);

var newTodo = await client.todo.create(todo);

Conclusion

Serverpod and Dart Frog take very different approaches to designing and building your Dart backend. While Dart Frog can give you some extra flexibility in structuring your server endpoints, Serverpod will give you a pragmatic, robust way of setting up your backend with a minimal amount of code.

--

--