Migration from Shelf
This guide helps you migrate from Shelf to Relic. While the core concepts remain similar (handlers, middleware, requests, and responses), Relic introduces improvements in type safety and developer experience that require some changes to your code.
Why migrate?
Relic was developed to meet the needs of Serverpod for a more modern web server foundation with stronger type safety. Shelf has been an excellent foundation for Dart web servers, but architectural decisions made years ago limit its ability to take advantage of modern Dart features.
Migration overview
Use this quick plan to get your app running on Relic. The detailed sections below show code for each step.
-
Update dependencies: Remove
shelf,shelf_router,shelf_web_socket. Addrelicto the dependencies. -
Bootstrap the server: Replace
shelf_io.serve()withRelicApp().serve()if using the io adapter, or integrate RelicApp into your hosting environment as needed. -
Keep handlers as
Response handler(Request request). Handlers in Relic receive aRequestand return aResult(usually aResponsesubclass). -
Switch to Relic routing: Replace Router from shelf_router with
RelicApp'sget,post,put, ordeletemethods. Replace<id>path parameters with:idand read them viarequest.pathParameters.raw[#id]. -
Create responses with
Body: ReplaceResponse.ok('text')withResponse.ok(body:...). Let Relic managecontent-lengthandcontent-typethrough theBodyclass. -
Replace header access: Replace string lookups like
request.headers['cookie']with typed accessors such asrequest.headers.cookie. -
Replace middleware and scoping: Replace
Pipeline().addMiddleware(...)withrouter.use(...)and attach handlers under that path. -
Replace
request.contextusage: Replacerequest.change(...)and manual casts withContextProperty<T>()\s .setorgetmethods on the context. -
Update WebSockets: Replace
webSocketHandlerand useRelicWebSocketfor handling events and sending data.
Detailed migration steps
1. Update dependencies
In your pubspec.yaml, remove Shelf packages and add Relic:
Shelf:
dependencies:
shelf: <shelf_version>
shelf_router: <shelf_router_version>
shelf_web_socket: <shelf_web_socket_version>
Relic:
dependencies:
relic: <latest_version>
2. Bootstrap the server
Shelf:
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
void main() async {
final app = Router();
// Add routes...
await io.serve(app, 'localhost', 8080);
}
Relic:
import 'package:relic/io_adapter.dart';
import 'package:relic/relic.dart';
Future<void> main() async {
final app =
RelicApp()..get('/users/:id', (final Request request) {
final id = request.rawPathParameters[#id];
final name = request.url.queryParameters['name'] ?? 'Unknown';
return Response.ok(body: Body.fromString('User $id: $name'));
});
await app.serve(address: InternetAddress.loopbackIPv4, port: 8080);
}
3. Update handler signatures
Shelf:
import 'package:shelf/shelf.dart';
Response handler(Request request) {
return Response.ok('Hello from Shelf!');
}
Relic:
Response handler(Request request) {
return Response.ok(body: Body.fromString('Hello from Relic!'));
}
4. Switch to Relic routing
Shelf:
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
final router = Router()
..get('/users/<id>', (Request request, String id) {
return Response.ok('User $id');
});
Relic:
import 'package:relic/relic.dart';
final router = RelicApp()
..get('/users/:id', (Request request) {
final id = request.pathParameters.raw[#id];
return Response.ok(body: Body.fromString('User $id'));
});
5. Create responses with Body
Shelf accepts strings directly and manages headers separately:
// Shelf - the body can be a plain string.
final response = Response.ok('Hello, World!');
// Content-Type is set separately in headers.
final response = Response.ok(
'<html>...</html>',
headers: {'content-type': 'text/html'},
);
Relic uses an explicit body type that unifies content, encoding, and MIME type:
// Relic - explicit Body object is required.
final response = Response.ok(
body: Body.fromString('Hello, World!'),
);
// Content-Length is automatically calculated and Content-Type and
// encoding are part of the Body.
final response = Response.ok(
body: Body.fromString('<html>...</html>', mimeType: MimeType.html),
);
6. Replace header access
Shelf uses string-based headers with manual parsing:
final contentType = request.headers['content-type']; // String?
final cookies = request.headers['cookie']; // String?
final date = request.headers['date']; // String?
Relic provides type-safe headers with automatic validation:
final contentType = request.body.bodyType?.mimeType; // MimeType?
final cookies = request.headers.cookie; // CookieHeader?
final date = request.headers.date; // DateTime?
7. Replace middleware and scoping
Shelf:
final app = Router()
..get('/api/users', (Request request) {
return Response.ok('User data');
});
final handler = Pipeline()
.addMiddleware(logRequests())
.addMiddleware(authentication())
.addHandler(app);
Relic:
final app = RelicApp()
..use('/', logRequests())
..use('/api', authentication())
..get('/api/users', (Request request) async {
return Response.ok(body: Body.fromString('User data'));
});
8. Replace request.context usage
Shelf:
// Shelf - Dynamic types.
final modifiedRequest = request.change(context: {
'user': currentUser,
'session': sessionData,
});
// Later...
final user = request.context['user'] as User?; // Manual casting
Relic:
// Relic - Type-safe.
final userProperty = ContextProperty<User>();
final sessionProperty = ContextProperty<Session>();
// Add an extension method for convenient access.
extension AuthContext on Request {
User get currentUser => userProperty[this];
Session get session => sessionProperty[this];
}
// Get values in a type-safe way.
final user = request.currentUser;
final session = request.session;
9. Update WebSockets
Shelf requires a separate package:
import 'package:shelf_web_socket/shelf_web_socket.dart';
var handler = webSocketHandler((webSocket) {
webSocket.stream.listen((message) {
print('Received: $message');
});
webSocket.sink.add('Hello!');
});
Relic has WebSockets built-in without the need for a separate package:
WebSocketUpgrade websocketHandler(final Request request) {
return WebSocketUpgrade((final ws) async {
ws.events.listen((final event) {
log('Received: $event');
});
ws.trySendText('Hello!');
ws.sendText('Hello!');
});
}
Example comparison
Here is a complete example showing the differences between Shelf and Relic.
Shelf version
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
void main() async {
final router = Router()
..get('/users/<id>', (Request request, String id) {
final name = request.url.queryParameters['name'] ?? 'Unknown';
return Response.ok('User $id: $name');
});
final handler = Pipeline()
.addMiddleware(logRequests())
.addHandler(router);
await shelf_io.serve(handler, 'localhost', 8080);
}
Relic version
import 'package:relic/io_adapter.dart';
import 'package:relic/relic.dart';
Future<void> main() async {
final app =
RelicApp()..get('/users/:id', (final Request request) {
final id = request.rawPathParameters[#id];
final name = request.url.queryParameters['name'] ?? 'Unknown';
return Response.ok(body: Body.fromString('User $id: $name'));
});
await app.serve(address: InternetAddress.loopbackIPv4, port: 8080);
}
Unlike Shelf's Pipeline().addMiddleware(), which runs for all requests (including 404s), Relic's .use('/', ...) only executes middleware for requests that match a route. Unmatched requests (404s) bypass middleware and go directly to the fallback handler.