shelf_plus 1.0.2 shelf_plus: ^1.0.2 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;
}
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
Source | Result | Use case |
---|---|---|
String |
Shelf Response |
Respond with a text (text/plain) |
Uint8List , Stream<List<int>> |
Shelf Response |
Respond with binary (application/octet-stream) |
Map<String, dynamic> , List<dynamic>> |
Shelf Response |
Respond with JSON (application/json) |
Any Type having a toJson() method |
Map<String, dynamic> , List<dynamic>> (expected) |
Provide serialization support for classes |
Shelf Handler |
Shelf Response |
Processing Shelf-based Middleware or Handler |
File (dart:io) |
Shelf Response |
Respond with file contents (using shelf_static) |
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'));
return app;
}
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, 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;
});
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());
}
}
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
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 | Enable hot-reload |
You can override the default values with optional parameters in the shelfRun()
function.
void main() => shelfRun(init, defaultBindPort: 3000);
Examples #
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;
}
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);
}