shelf_rest 0.3.2 shelf_rest: ^0.3.2 copied to clipboard
Shelf components that makes it easy to create uniform, hierarchical REST resources.
REST Handler for Dart Shelf #
Introduction #
Provides Shelf components that makes it easy to create uniform, hierarchical REST resources with minimal boilerplate.
shelf_rest
is a drop in replacement of shelf_route. It supports all the functionality of shelf_route
with many additions to reduce boilerplate.
###Routing Choices###
There are a number of choices for routing in the shelf
world. Here is a simple guide to help you choose between a few of them.
- shelf_route. Good choice if:
- you want a powerful router with a fluent api
- you don't want to use mirrors or annotations
- you prefer a bit more boilerplate over any magic that comes with mirrors
- shelf_rest. Good choice if:
- you want all the features of
shelf_route
plus - you are happy to use annotations (supported by mirrors) to significantly reduce boilerplate
- you like consistency in your REST APIs and like support to help with that
- you want all the features of
- mojito. Good choice if:
- you want all the features of shelf_rest plus
- you want a light framework that provides a fluent api on many other shelf components for things like:
- authentication & authorisation;
- serving static resources via the filesystem or via pub serve;
- oauth;
- logging and more
- authentication & authorisation;
In short, if you want to build your own stack then shelf_route
and shelf_rest
will likely suit you better. If you want a more fully featured framework, whilst still being highly extensible, then mojito
is the better option.
To get a good overview of the options you have, read the blog post Routing Options in Shelf.
Basic Usage #
Instead of importing shelf_route
import 'package:shelf_route/shelf_route.dart';
you import shelf_rest
import 'package:shelf_rest/shelf_rest.dart';
Note: don't import both at the same time.
If you wish, you can continue to use it exactly the same as shelf_route
, such as.
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_rest/shelf_rest.dart';
void main() {
var myRouter = router()
..get('/accounts/{accountId}', (Request request) {
var account =
new Account.build(accountId: getPathParameter(request, 'accountId'));
return new Response.ok(JSON.encode(account));
});
printRoutes(myRouter);
io.serve(myRouter.handler, 'localhost', 8081);
}
class Account {
final String accountId;
Account.build({this.accountId});
Account.fromJson(Map json) : this.accountId = json['accountId'];
Map toJson() => {'accountId': accountId};
}
Using normal Dart functions as Handlers #
As shelf_rest
automatically bundles shelf_bind you can now remove much of this boiler plate.
var myRouter = router()
..get('accounts/{accountId}',
(String accountId) => new Account.build(accountId: accountId));
Here the accountId
path parameter was automatically extracted from the request and passed in as a variable to the handler function. Additionally, the returned account is automatically converted into JSON.
See the documentation for shelf_bind for more details on the features you an use with your handlers.
Grouping routes into classes #
You can group routes into classes and mount these at a given subpath using the addAll
method.
class AccountResource {
void createRoutes(Router r) {
r..get('{accountId}', (String accountId) => new Account.build(accountId: accountId));
}
}
void main() {
var myRouter = router()..addAll(new AccountResource(), path: 'accounts');
printRoutes(myRouter);
io.serve(myRouter.handler, 'localhost', 8081);
}
As the createRoutes
method in UserResource
takes a single argument of type Router
, this will automatically be called.
Using Route annotations #
Instead of implementing a method that takes a Router
, like createRoutes
above, you can use a Get
annotation.
class AccountResource {
@Get('{accountId}')
Account find(String accountId) => new Account.build(accountId: accountId);
}
Annotations exist for all the methods on Router
such as @Get
, @Post
, @Put
, @Delete
and@AddAll
and these annotations support exactly the same arguments as the corresponding methods.
The @AddAll
annotation is used to add nested routes (child resources). For example
class AccountResource {
@AddAll(path: 'deposits')
DepositResource deposits() => new DepositResource();
}
Note: @AddAll
is currently only supported on methods. Support on getters likely in a future version
Using the RestResource annotation #
Most REST resources tend to include many of the standard CRUD operations.
To further reduce boilerplate and help enforce consistency, shelf_rest
has special support for implementing these CRUD operations.
For example a RESTful resource for a bank account
might have the following types of operations
Search Accounts
GET /accounts?name='Freddy'
Fetch a single Account
GET /accounts/1234
Create an Account
POST /accounts
Update an Account
PUT /accounts/1234
Delete an Account
DELETE /accounts/1234
This is the standard pattern in shelf_rest
and can be implemented as follows
@RestResource('accountId')
class AccountResource {
List<Account> search(String name) => .....;
Account create(Account account) => .....;
Account update(Account account) => .....;
Account find(String accountId) => ...;
void delete(String accountId) => ...;
}
The @RestResource('accountId')
annotation is used to denote classes that support the standard CRUD operations and tells shelf_rest
to use accountId
as the path variable. The route for DELETE would look like
DELETE /accounts/{accountId}
shelf_rest
follows a standard naming convention to minimise configuration. This also serves to promote consistency in how you name your methods.
You can however override the default naming using the ResourceMethod
annotation
@ResourceMethod(operation: RestOperation.FIND)
Account fetchAccount(String accountId) => ...;
Hierarchical Resources #
It is common to create hierarchical REST resources.
For example, we might want to allow deposits to be made to our account as follows
PUT -> /accounts/1234/deposits/999
You add child resources using the standard @AddAll
annotation described above.
@RestResource('accountId')
class AccountResource {
....
@AddAll(path: 'deposits')
DepositResource deposits() => new DepositResource();
}
Where the DepositResource
might look like
@RestResource('depositId')
class DepositResource {
@ResourceMethod(method: 'PUT')
Deposit create(Deposit deposit) => ...;
}
Note, that the default HTTP method for a create
operation is POST
. PUT
is often used when we know the primary key of the resource when we invoke the create.
In shelf_rest
we do that by overriding the HTTP method with the ResourceMethod
annotation.
To see this in action we use the printRoutes
function
printRoutes(router);
You can see that the following routes were created
GET -> /accounts{?name} => bound to search method
POST -> /accounts => bound to create method
GET -> /accounts/{accountId} => bound to find method
PUT -> /accounts/{accountId} => bound to update method
DELETE -> /accounts/{accountId} => bound to delete method
PUT -> /accounts/{accountId}/deposits/{depositId} => bound to create method of DepositResource
Note that any arguments that are not existing path variables will be added to the query of the uri template. So
List<Account> search(String name) => .....;
produces
GET -> /accounts{?name}
Middleware #
You can add middleware that will be included in the route created for a resource method using the ResourceMethod
annotation.
@ResourceMethod(middleware: logRequests)
Account find(String accountId) => ...;
Similarly you can add them to all the Route annotations like Get
and AddAll
. For example
@AddAll(path: 'deposits', middleware: logRequests)
DepositResource deposits() => new DepositResource();
Validation #
As shelf_bind is used to create Shelf handlers from the resource methods, validation of request parameters comes for free (courtesy of constrain).
See the shelf_bind and constrain doco for details.
By default, validation is turned off. You can turn validation on for specific resource methods
@ResourceMethod(validateParameters: true)
Account find(String accountId) => ...;
You can also turn it on at any level of the router hierarchy by passing creating a new handlerAdapter
. For example you can turn it on for all routes as follows
var router = router('/accounts', new AccountResource(),
handlerAdapter: handlerAdapter(validateParameters: true,
validateReturn: true);
HATEOAS Support #
shelf_rest
has support for returning responses with HATEOAS links. The models for manipulating these links are in the hateoas_models package and may also be used on the client.
To use, simply add an argument to your handler methods of type ResourceLinksFactory
. For example
AccountResourceModel find(
String accountId, ResourceLinksFactory linksFactory) =>
new AccountResourceModel(
new Account.build(accountId: accountId), linksFactory(accountId));
The AccountResourceModel
here is just a simple class that includes both the Account
and the HATEOAS
resource links.
class AccountResourceModel extends ResourceModel<Account> {
final Account account;
AccountResourceModel(this.account, ResourceLinks links) : super(links);
Map toJson() => super.toJson()..addAll({'account': account.toJson()});
}
A typical response for the find
operation looks like
{
"account": {
"accountId": "123",
"name": 'fred'
},
"links": [
{
"href": "123",
"rel": "self"
},
{
"href": "123",
"rel": "update"
},
{
"href": "123/deposits/{?deposit}",
"rel": "deposits.create"
}
]
}
Mix and Match #
All the different forms of specifying routes can be used together. A common approach is to use the @RestResource
approach for the standard CRUD
operations together with the @Get
, @Post
, @Put
, @Delete
annotations for operations that don't fit the standard model.
Using methods that take Router
as their only argument (called RouteableFunction
s) provides a more fluent alternative. Particularly useful with a framework like mojito that extends the Router
with fluent apis for creating oauth routes for example.
Conventions #
shelf_rest
uses the following conventions by default. Each can be overriden with annotations.
- create ... POST
TODO: more doco