Dart en el servidor: Dart Frog y Shelf para APIs sin salir del ecosistema Dart

Dart no es solo el lenguaje de Flutter. Desde sus primeras versiones ya tenía la capacidad de ejecutarse en el servidor, y aunque nunca ha desplazado a Node.js, Go o Python en ese nicho, hay un escenario donde tiene mucho sentido: cuando ya tienes un equipo de Flutter y quieres reutilizar lógica de negocio en el backend sin añadir un lenguaje nuevo al stack.

Shelf: la librería HTTP base

Shelf es la librería HTTP oficial de Dart, disponible en pub.dev/packages/shelf. Es la base sobre la que se construyen la mayoría de los frameworks de servidor en Dart. Shelf funciona con un modelo de middlewares encadenados, parecido a Express en Node.js o a WSGI en Python.

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';

void main() async {
  final router = Router();

  router.get('/hello', (Request request) {
    return Response.ok('Hola desde Dart');
  });

  router.get('/users/<id>', (Request request, String id) {
    return Response.ok('Usuario: $id');
  });

  final handler = Pipeline()
      .addMiddleware(logRequests())
      .addHandler(router.call);

  final server = await io.serve(handler, 'localhost', 8080);
  print('Servidor en ${server.address.host}:${server.port}');
}

Shelf es deliberadamente minimalista. No impone estructura de directorios, no genera código, no tiene ORM integrado. Es el nivel más bajo razonable para construir encima. Si necesitas más estructura, ahí entra Dart Frog.

Dart Frog: framework de Very Good Ventures

Dart Frog es un framework de servidor para Dart creado por Very Good Ventures, el estudio de Flutter conocido por varios paquetes populares del ecosistema. Está construido sobre Shelf y añade convenciones claras: estructura de archivos basada en rutas (como Next.js), generación de código, hot reload en desarrollo y soporte para middleware por ruta.

// routes/index.dart — ruta raíz
import 'package:dart_frog/dart_frog.dart';

Response onRequest(RequestContext context) {
  return Response.json(body: {'message': 'Hola desde Dart Frog'});
}

// routes/users/[id].dart — ruta dinámica
import 'package:dart_frog/dart_frog.dart';

Response onRequest(RequestContext context, String id) {
  final method = context.request.method;
  return switch (method) {
    HttpMethod.get => Response.json(body: {'id': id}),
    HttpMethod.delete => Response.json(body: {'deleted': id}),
    _ => Response(statusCode: 405),
  };
}

La estructura de archivos define las rutas automáticamente. Un archivo en routes/users/[id].dart responde a /users/:id. Esto hace que sea muy fácil orientarse en el proyecto sin necesidad de un router configurado manualmente.

Middleware en Dart Frog

El middleware en Dart Frog se puede aplicar a nivel global o por directorio de rutas:

// middleware.dart — middleware global
import 'package:dart_frog/dart_frog.dart';

Handler middleware(Handler handler) {
  return handler.use(requestLogger());
}

// routes/admin/_middleware.dart — solo para /admin/*
import 'package:dart_frog/dart_frog.dart';

Handler middleware(Handler handler) {
  return (context) async {
    final token = context.request.headers['Authorization'];
    if (token != 'Bearer mi-token-secreto') {
      return Response(statusCode: 401);
    }
    return handler(context);
  };
}

Compartir código con la app Flutter

La ventaja principal de usar Dart en el servidor cuando ya tienes Flutter es poder compartir código. Los modelos de datos, las reglas de validación y cierta lógica de negocio pueden vivir en un paquete Dart que importen tanto la app Flutter como el servidor.

// packages/shared_models/lib/user.dart
class User {
  final String id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => User(
        id: json['id'] as String,
        name: json['name'] as String,
        email: json['email'] as String,
      );

  Map<String, dynamic> toJson() => {
        'id': id,
        'name': name,
        'email': email,
      };
}

Este paquete puede estar en un monorepo con la app Flutter y el servidor Dart Frog, usando las workspace dependencies de Dart. Cada vez que cambias el modelo, el compilador te avisa inmediatamente de cualquier inconsistencia tanto en el cliente como en el servidor.

Limitaciones reales

Dart en el servidor tiene limitaciones que conviene conocer. El ecosistema de paquetes para tareas de backend (ORMs, drivers de base de datos, clientes de mensajería) es más pequeño que el de Node.js o Python. Para PostgreSQL existe postgres, para Redis existe redis, pero si necesitas algo específico puede que tengas que escribir el cliente tú mismo o usar la interfaz HTTP de la API.

El despliegue también requiere más configuración que un runtime Node.js estándar. Puedes compilar a un binario nativo con dart compile exe y desplegarlo en cualquier servidor Linux sin dependencias, lo que es una ventaja, pero las plataformas PaaS no suelen tener soporte nativo para Dart como lo tienen para Node.js o Python.

// Compilar a binario nativo
// dart compile exe bin/server.dart -o bin/server

// Con Dart Frog
// dart_frog build
// El resultado es un ejecutable en build/bin/server

Cuándo tiene sentido

Dart Frog o Shelf tienen sentido cuando el equipo ya es de Dart/Flutter, la API es relativamente sencilla (CRUD, websockets, notificaciones push) y la reutilización de código entre cliente y servidor aporta valor real. Para una API pública con miles de peticiones por segundo donde el rendimiento bruto importa, Go o Rust seguirán siendo mejores opciones.

Si te interesa el lado del rendimiento y los tests de la app Flutter, puede ser útil ver también el artículo sobre testing en Flutter, donde se cubre cómo probar la lógica que podrías compartir entre cliente y servidor.

Imagen: Pexels / Digital Buggu

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP