shelf_plus 1.10.0 copy "shelf_plus: ^1.10.0" to clipboard
shelf_plus: ^1.10.0 copied to clipboard

Shelf Plus is a quality of life addon for your server-side development within the Shelf platform.

Shelf Plus #

Shelf Plus is a quality of life addon for your server-side development within the Shelf platform. It's a great base to start off your apps fast, while maintaining full compatibility with the Shelf ecosystem.

import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  var app = Router().plus;

  app.get('/', () => 'Hello World!');

  return app.call;
}

It comes with a lot of awesome features, like zero-configuration initializer, build-in hot-reload and a super powerful and intuitive router upgrade. Continue reading and get to know why you can't ever code without Shelf Plus.

Table of Contents #

Router Plus

Middleware collection

Request body handling

Shelf Run

Examples

Router Plus #

Router Plus is a high-level abstraction layer sitting directly on shelf_router. It shares the same routing logic but allows you to handle responses in a very simple way.

var app = Router().plus;

app.use(middleware());

app.get('/text', () => 'I am a text');

app.get(
    '/html/<name>', (Request request, String name) => '<h1>Hello $name</h1>',
    use: typeByExtension('html'));

app.get('/file', () => File('path/to/file.zip'));

app.get('/person', () => Person(name: 'John', age: 42));

The core mechanic is called ResponseHandler which continuously refines a data structure, until it resolves in a Shelf Response. This extensible system comes with support for text, json, binaries, files, json serialization and Shelf Handler.

You can access the Router Plus by calling the .plus getter on a regular Shelf Router.

var app = Router().plus;

Routes API #

The API mimics the Shelf Router methods. You basically use an HTTP verb, define a route to match and specify a handler, that generates the response.

app.get('/path/to/match', () => 'a response');

You can return any type, as long the ResponseHandler mechanism has a capable resolver to handle that type.

If you need the Shelf Request object, specify it as the first parameter. Any other parameter will match the route parameters, if defined.

app.get('/minimalistic', () => 'response');

app.get('/with/request', (Request request) => 'response');

app.get('/clients/<id>', (Request request, String id) => 'response: $id');

app.get('/customer/<id>', (Request request) {
  // alternative access to route parameters
  return 'response: ${request.routeParameter('id')}';
});

Middleware #

Router Plus provides several options to place your middleware (Shelf Middleware).

var app = Router().plus;

app.use(middlewareA); // apply to all routes

// apply to a single route
app.get('/request1', () => 'response', use: middlewareB);

// combine middleware with + operator
app.get('/request2', () => 'response', use: middlewareB + middlewareC);

You can also apply middleware dynamically inside a route handler, using the >> operator.

app.get('/request/<value>', (Request request, String value) {
  return middleware(value) >> 'response';
});

ResponseHandler #

ResponseHandler process the return value of a route handler, until it matches a Shelf Response.

Build-in ResponseHandler

Return type Use case
String Respond with a text (text/plain)
Uint8List, Stream<List<int>> Respond with binary (application/octet-stream)
Map<String, dynamic>, List<dynamic>> Respond with JSON (application/json)
Any Type having a toJson() method Serialization support for classes
File (dart:io) Respond with file contents (using shelf_static)
WebSocketSession (shelf_plus) Create a websocket connection (using shelf_web_socket)
Handler (shelf) Processing Shelf-based Middleware or Handler

Example:

import 'dart:io';

import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  var app = Router().plus;

  app.get('/text', () => 'a text');

  app.get('/binary', () => File('data.zip').openRead());

  app.get('/json', () => {'name': 'John', 'age': 42});

  app.get('/class', () => Person('Theo'));

  app.get('/list-of-classes', () => [Person('Theo'), Person('Bob')]);

  app.get('/iterables', () => [1, 10, 100].where((n) => n > 9));

  app.get('/handler', () => typeByExtension('html') >> '<h1>Hello</h1>');

  app.get('/file', () => File('thesis.pdf'));

  app.get(
      '/websocket',
      () => WebSocketSession(
            onOpen: (ws) => ws.send('Hello!'),
            onMessage: (ws, data) => ws.send('You sent me: $data'),
            onClose: (ws) => ws.send('Bye!'),
          ));

  return app.call;
}

class Person {
  final String name;

  Person(this.name);

  // can be generated by tools (i.e. json_serializable package)
  Map<String, dynamic> toJson() => {'name': name};
}

Custom ResponseHandler

You can add your own ResponseHandler by using a Shelf Middleware created with the .middleware getter on a ResponseHandler function.

// define custom ResponseHandler
ResponseHandler catResponseHandler = (Request request, dynamic maybeCat) =>
    maybeCat is Cat ? maybeCat.interact() : null;

// register custom ResponseHandler as middleware
app.use(catResponseHandler.middleware);

app.get('/cat', () => Cat());
class Cat {
  String interact() => 'Purrrrr!';
}

Cascading multiple routers #

Router Plus is compatible to a Shelf Handler. So, you can also use it in a Shelf Cascade. This package provides a cascade() function, to quickly set up a cascade.

import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  var app1 = Router().plus;
  var app2 = Router().plus;

  app1.get('/maybe', () => Response.notFound('no idea'));

  app2.get('/maybe', () => 'got it!');

  return cascade([app1.call, app2.call]);
}
import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  var app1 = Router().plus;
  var app2 = Router().plus;

  app1.get('/maybe', () => Response.notFound('no idea'));

  app2.get('/maybe', () => 'got it!');

  return cascade([app1, app2]);
}

Middleware collection #

This package comes with additional Shelf Middleware to simplify common tasks.

setContentType #

Sets the content-type header of a Response to the specified mime-type.

app.get('/one', () => setContentType('application/json') >> '1');

app.get('/two', () => '2', use: setContentType('application/json'));

typeByExtension #

Sets the content-type header of a Response to the mime-type of the specified file extension.

app.get('/', () => '<h1>Hi!</h1>', use: typeByExtension('html'));

download #

Sets the content-disposition header of a Response, so browsers will download the server response instead of displaying it. Optionally you can define a specific file name.

app.get('/wallpaper/download', () => File('image.jpg'), use: download());

app.get('/invoice/<id>', (Request request, String id) {
  File document = pdfService.generateInvoice(id);
  return download(filename: 'invoice_$id.pdf') >> document;
});
app.get('/wallpaper/download', () => File('image.jpg'), use: download());

app.get('/invoice/<id>', (Request request, String id) {
  File document = pdfService.generateInvoice(id);
  return download(filename: 'invoice_$id.pdf') >> document;
});

Request body handling #

Shelf Plus provides an extensible mechanism to process the HTTP body of a request. You can access it by calling the .body getter on a Shelf Request.

It comes with build-in support for text, JSON and binary.

app.post('/text', (Request request) async {
  var text = await request.body.asString;
  return 'You send me: $text';
});

app.post('/json', (Request request) async {
  var person = Person.fromJson(await request.body.asJson);
  return 'You send me: ${person.name}';
});

Object deserialization #

A recommended way to deserialize a json-encoded object is to provide a reviver function, that can be generated by code generators.

var person = await request.body.as(Person.fromJson);
class Person {
  final String name;

  Person({required this.name});

  // created with tools like json_serializable package
  static Person fromJson(Map<String, dynamic> json) {
    return Person(name: json['name']);
  }
}

Custom accessors for model classes #

You can add own accessors for model classes by creating an extension on RequestBodyAccessor.

extension PersonAccessor on RequestBodyAccessor {
  Future<Person> get asPerson async => Person.fromJson(await asJson);
}
app.post('/person', (Request request) async {
  var person = await request.body.asPerson;
  return 'You send me: ${person.name}';
});

Custom accessors for third party body parser #

You can plug-in any other body parser by creating an extension on RequestBodyAccessor.

extension OtherFormatBodyParserAccessor on RequestBodyAccessor {
  Future<OtherBodyFormat> get asOtherFormat async {
    return ThirdPartyLib().process(request.read());
  }
}
extension OtherFormatBodyParserAccessor on RequestBodyAccessor {
  Future<OtherBodyFormat> get asOtherFormat async {
    return ThirdPartyLib().process(request.read());
  }
}

Shelf Run #

Shelf Run is zero-configuration web-server initializer with hot-reload support.

import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  return (Request request) => Response.ok('Hello!');
}

It's important to use a dedicated init function, returning a Shelf Handler, for hot-reload to work properly.

To enable hot-reload you need either run your app with the IDE's debug profile, or enable vm-service from the command line:

dart run --enable-vm-service my_app.dart

Custom configuration #

Shelf Run uses a default configuration, that can be modified via environment variables:

Environment variable Default value Description
SHELF_PORT 8080 Port to bind the shelf application to
SHELF_ADDRESS localhost Address to bind the shelf application to
SHELF_HOTRELOAD true Enables hot-reload
SHELF_SHARED false Enables shared option for multithreading

You can override the default values with optional parameters in the shelfRun() function.

void main() => shelfRun(init, defaultBindPort: 3000);

Multithreading #

Dart supports multithreading using isolates. This might increases the performance by utilizing more cores.

You can enable the shared by setting the defaultShared parameter or the SHELF_SHARED environment variable to true.

Example of an application using multiple isolates

import 'dart:isolate';
import 'package:shelf_plus/shelf_plus.dart';

void main() {
  const numberOfIsolates = 8;

  for (var i = 0; i < numberOfIsolates - 1; i++) {
    Isolate.spawn(spawnServer, null, debugName: i.toString()); // isolate 0..7
  }
  spawnServer(null); // use main isolate as the 8th isolate
}

void spawnServer(_) => shelfRun(init, defaultShared: true);

Handler init() {
  var app = Router().plus;

  app.get('/', () async {
    await Future.delayed(Duration(milliseconds: 500)); // simulate load
    return 'Hello from isolate: ${Isolate.current.debugName}';
  });

  return app.call;
}

You can test this application and compare different count of isolates:

xargs -I % -P 8 curl "http://localhost:8080" < <(printf '%s\n' {1..400})

Examples #

Enable CORS #

CORS can be enabled by using the shelf_cors_headers package:

import 'package:shelf_cors_headers/shelf_cors_headers.dart';
import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  final router = Router().plus;

  router.get('/', () => {'data': 'This API is CORS enabled.'});

  return corsHeaders() >> router.call;
}

Rest Service #

Implementation of a CRUD, rest-like backend service. (Full sources)

example_rest.dart

import 'dart:io';

import 'package:shelf_plus/shelf_plus.dart';

import 'person.dart';

void main() => shelfRun(init);

final data = <Person>[
  Person(firstName: 'John', lastName: 'Doe', age: 42),
  Person(firstName: 'Jane', lastName: 'Doe', age: 43),
];

Handler init() {
  var app = Router().plus;

  /// Serve index page of frontend
  app.get('/', () => File('frontend/page.html'));

  /// List all persons
  app.get('/person', () => data);

  /// Get specific person by id
  app.get('/person/<id>',
      (Request request, String id) => data.where((person) => person.id == id));

  /// Add a new person
  app.post('/person', (Request request) async {
    var newPerson = await request.body.as(Person.fromJson);
    data.add(newPerson);
    return {'ok': 'true', 'person': newPerson.toJson()};
  });

  /// Update an existing person by id
  app.put('/person/<id>', (Request request, String id) async {
    data.removeWhere((person) => person.id == id);
    var person = await request.body.as(Person.fromJson);
    person.id = id;
    data.add(person);
    return {'ok': 'true'};
  });

  /// Remove a specific person by id
  app.delete('/person/<id>', (Request request, String id) {
    data.removeWhere((person) => person.id == id);
    return {'ok': 'true'};
  });

  return app.call;
}

person.dart

import 'package:json_annotation/json_annotation.dart';
import 'package:uuid/uuid.dart';

part 'person.g.dart';

/// run 'dart run build_runner build' to update model

@JsonSerializable()
class Person {
  String? id;
  final String firstName;
  final String lastName;
  final int age;

  Person({
    String? id,
    required this.firstName,
    required this.lastName,
    required this.age,
  }) {
    this.id = id ?? Uuid().v4();
  }

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

  static Person fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
}

WebSocket chat server #

Implementation of a WebSocket-based chat application. (Full sources)

example_websocket_chat.dart

import 'dart:io';

import 'package:shelf_plus/shelf_plus.dart';

void main() => shelfRun(init);

Handler init() {
  var app = Router().plus;

  // HTML-based web client
  app.get('/', () => File('public/html_client.html'));

  // Track connected clients
  var users = <WebSocketSession>[];

  // Web socket route
  app.get(
    '/ws',
    () => WebSocketSession(
      onOpen: (ws) {
        // Join chat
        users.add(ws);
        users
            .where((user) => user != ws)
            .forEach((user) => user.send('A new user joined the chat.'));
      },
      onClose: (ws) {
        // Leave chat
        users.remove(ws);
        for (var user in users) {
          user.send('A user has left.');
        }
      },
      onMessage: (ws, dynamic data) {
        // Deliver messages to all users
        for (var user in users) {
          user.send(data);
        }
      },
    ),
  );

  return app.call;
}
97
likes
160
points
3.91k
downloads

Publisher

verified publisherfelix-blaschke.de

Weekly Downloads

Shelf Plus is a quality of life addon for your server-side development within the Shelf platform.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

mime_type, shelf, shelf_hotreload, shelf_router, shelf_static, shelf_web_socket, web_socket_channel

More

Packages that depend on shelf_plus