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
- Object deserialization
- Custom accessors for model classes
- Custom accessors for third party body parser
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;
}
Libraries
- shelf_plus
- Shelf Plus is a quality of life addon for your server-side development within the Shelf platform.