Un server Http in Dart

In questo articolo, esploreremo passo dopo passo come sviluppare un server HTTP in Dart. Partiremo dalle basi, illustrando come creare un server di base in grado di gestire le richieste in arrivo. Successivamente, approfondiremo argomenti più avanzati come la gestione delle rotte, l'elaborazione di richieste e risposte, e l'implementazione di middleware.


Inizializziamo il progetto

Per la creazione del nuovo progetto Dart, dal terminale di VSCode digitiamo i seguenti comandi


 dart create dart_server_http  cd dart_server_http code .


Aperto il progetto, rimuoviamo le cartelle lib e test (per il nostro scopo non ci occorrono) e modifichiamo il file bin/dart_server_http in questo modo


void main(List<String> arguments) {
 
}

La versione base

Per gestire le rotte (inizialmente solo quella principale "/"), aggiungiamo le dipendenze necessarie, lavorando sempre dal terminale:


 flutter pub add shelf
flutter pub add shelf_router


aggiungiamo quindi il Router(), definiamo la rotta "/" e l'handler che sarà invocato 


import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_router/shelf_router.dart';

final router = Router()
  ..get('/', _rootHandler)
  ;

Response _rootHandler(Request req) {
  return Response.ok('Hello, World!\n');
}


Modifichiamo il metodo main() in questo modo per avviare l'ascolto del server (chiaramente sarà un metodo asincrono):


void main(List<String> arguments) async {
  final ip = "127.0.0.1";
  final port = 8080;

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

  final server = await serve(handler, ip, port);
  print('Server listening ${server.address.host}:${server.port}');
}


Avviamo il progetto e il nostro server sarà pronto a rispondere all'indirizzo 127.0.0.1 e sulla porta 8080 con il classico Hello, World! . 


Aggiungiamo elementi avanzati

Nell'esempio precedente, abbiamo forzatamente indicato l'IP su cui lasciare in ascolto il server, ma possiamo anche aggiungere la dipendenza 


import 'dart:io';


ed accettare connessioni da qualsiasi indirizzo, con questa notazione


  final ip = InternetAddress.anyIPv4;


Allo stesso modo, possiamo eventualmente ricavare la porta dall'ambiente su cui sta girando il nostro server, con questa assegnazione 


final port = int.parse(Platform.environment['PORT'] ?? '8080');


Possiamo ancora utilizzare un middleware per avere un log di tutte le richieste ricevute dal server, modificando il nostro handler: 


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


e avere un log come questo:


Server listening 0.0.0.0:8080 2025-01-31T14:26:11.287931 0:00:00.005945 GET [404] /flutter_service_worker.js 2025-01-31T14:26:14.993291 0:00:00.003056 GET [200] / 2025-01-31T14:26:17.044027 0:00:00.000067 GET [404] /flutter_service_worker.js 2025-01-31T14:26:24.823039 0:00:00.000336 GET [200] / 2025-01-31T14:26:26.853715 0:00:00.000052 GET [404] /flutter_service_worker.js


Da ultimo possiamo aggiungere una rotta residuale per tutte le URL non gestite, modificando il nostro router


final router = Router()
  ..get('/', _rootHandler)
  ..all('/<ignored|.*>', (Request request) {
    return Response.notFound('Page not found');
  });


e quindi avere una risposta di questo tipo sulle rotte non definite:


E infine possiamo indicare che la risposta del server non sia semplice testo, ma contenuto HTML, aggiungendo il corretto Content-Type


Response _rootHandler(Request req) {
  return Response.ok('<h1>Hello, World!</h1>\n',
      headers: {'Content-Type': 'text/html'});
}


Lavoriamo su rotte API

Potrebbe essere interessanti gestire delle rotte API che ritornino dati in formato JSON. Aggiungiamo una nuova dipendenza


import 'dart:convert';


quindi definiamo una nuova classe che espone un nuovo router specifico:


class Api {
  List<int> items = [1, 2, 3, 4, 5];

  Future<Response> _items(Request request) async {
    return Response.ok(jsonEncode(items),
        headers: {'Content-Type': 'application/json'});
  }

  Router get router {
    final router = Router();

    router.get('/items', _items);
    router.all('/<ignored|.*>', (Request request) => Response.notFound('null'));

    return router;
  }
}


Per semplicità i nostri dati sono rappresentati da un semplice array di numeri interi che viene opportunamente codificato in formato JSON.

Montiamo questa classe nel router principale:

final router = Router()
  ..get('/', _rootHandler)
  ..mount('/api/v1.0/', Api().router.call)
  ..all('/<ignored|.*>', (Request request) {
    return Response.notFound('Page not found');
  });

e quindi testiamo l'URL


Se volessimo gestire anche una chiamata POST, sarebbe sufficiente gestire la nuova rotta ad esempio in questo modo:


    ...
    router.post('/items', (Request request) async {
      final body = await request.readAsString();
      final item = int.parse(body);
      items.add(item);
      return Response.ok(jsonEncode(items),
          headers: {'Content-Type': 'application/json'});
    });
    ...


Conclusioni

Gli appunti presentati sono un buon punto di partenza per fare qualche test e possono essere facilmente ampliati. Potete trovare il codice tra le mie repo GitHub: https://github.com/luigimicco/dart_server_http

Commenti