relic-shelf-migration

star 124

Migrate a Dart web server from Shelf to Relic. Side-by-side reference for every API change. Use when converting Shelf code to Relic, replacing shelf/shelf_router/shelf_web_socket imports, or upgrading an existing server.

serverpod By serverpod schedule Updated 2/26/2026

name: relic-shelf-migration description: Migrate a Dart web server from Shelf to Relic. Side-by-side reference for every API change. Use when converting Shelf code to Relic, replacing shelf/shelf_router/shelf_web_socket imports, or upgrading an existing server.

Shelf to Relic Migration

Relic replaces shelf, shelf_router, and shelf_web_socket with a single package. Core concepts (handlers, middleware, requests, responses) carry over with improved type safety.

1. Dependencies

Remove Shelf packages, add Relic:

# Before (Shelf)
dependencies:
  shelf: <version>
  shelf_router: <version>
  shelf_web_socket: <version>

# After (Relic)
dependencies:
  relic: <latest_version>

2. Server bootstrap

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/relic.dart';

Future<void> main() async {
  final app = RelicApp()
    ..get('/users/:id', (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. Handlers

Shelf:

Response handler(Request request) {
  return Response.ok('Hello from Shelf!');
}

Relic: Handler returns Result (usually Response). Body requires explicit Body object:

Response handler(Request request) {
  return Response.ok(body: Body.fromString('Hello from Relic!'));
}

4. Routing

Shelf: Path params use <id> and are passed as handler arguments:

final router = Router()
  ..get('/users/<id>', (Request request, String id) {
    return Response.ok('User $id');
  });

Relic: Path params use :id and are read from request.pathParameters:

final router = RelicApp()
  ..get('/users/:id', (Request request) {
    final id = request.pathParameters.raw[#id];
    return Response.ok(body: Body.fromString('User $id'));
  });

5. Responses with Body

Shelf: Accepts plain strings, sets Content-Type via headers map:

Response.ok('Hello, World!');

Response.ok(
  '<html>...</html>',
  headers: {'content-type': 'text/html'},
);

Relic: Requires Body object. Content-Length is automatic. MIME type is auto-detected or explicit:

Response.ok(body: Body.fromString('Hello, World!'));

Response.ok(
  body: Body.fromString('<html>...</html>', mimeType: MimeType.html),
);

6. Headers

Shelf: String-based, manual parsing:

final contentType = request.headers['content-type']; // String?
final cookies = request.headers['cookie'];            // String?
final date = request.headers['date'];                 // String?

Relic: Type-safe accessors with automatic parsing:

final contentType = request.body.bodyType?.mimeType;  // MimeType?
final cookies = request.headers.cookie;                // CookieHeader?
final date = request.headers.date;                     // DateTime?

7. Middleware

Shelf: Pipeline applies middleware to all requests including 404s:

final app = Router()
  ..get('/api/users', (Request request) {
    return Response.ok('User data');
  });

final handler = Pipeline()
  .addMiddleware(logRequests())
  .addMiddleware(authentication())
  .addHandler(app);

Relic: router.use() scopes middleware by path and only runs on matched routes:

final app = RelicApp()
  ..use('/', logRequests())
  ..use('/api', authentication())
  ..get('/api/users', (Request request) async {
    return Response.ok(body: Body.fromString('User data'));
  });

Unmatched requests (404s) bypass middleware and go directly to the fallback handler.

8. Context

Shelf: Dynamic map with manual casting:

final modifiedRequest = request.change(context: {
  'user': currentUser,
  'session': sessionData,
});

// Later...
final user = request.context['user'] as User?;

Relic: Type-safe ContextProperty with extension methods:

final userProperty = ContextProperty<User>('user');
final sessionProperty = ContextProperty<Session>('session');

extension AuthContext on Request {
  User get currentUser => userProperty[this];
  Session get session => sessionProperty[this];
}

// Set in middleware
userProperty[req] = authenticatedUser;

// Read in handler -- type-safe, no casting
final user = req.currentUser;
final session = req.session;

9. WebSockets

Shelf: Requires separate shelf_web_socket 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: Built-in, return WebSocketUpgrade from any handler:

WebSocketUpgrade websocketHandler(Request request) {
  return WebSocketUpgrade((ws) async {
    ws.events.listen((event) {
      log('Received: $event');
    });
    ws.trySendText('Hello!');
    ws.sendText('Hello!');
  });
}

Complete side-by-side example

Shelf

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

import 'package:relic/relic.dart';

Future<void> main() async {
  final app = RelicApp()
    ..use('/', logRequests())
    ..get('/users/:id', (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);
}

Quick reference

Shelf Relic
import 'package:shelf/shelf.dart' import 'package:relic/relic.dart'
shelf_io.serve(handler, host, port) RelicApp().serve(address: addr, port: port)
Response.ok('text') Response.ok(body: Body.fromString('text'))
Router()..get('/path/<id>', (req, id) {...}) RelicApp()..get('/path/:id', (req) {...})
request.headers['name'] request.headers.name (typed)
Pipeline().addMiddleware(mw).addHandler(h) app.use('/', mw)
request.change(context: {...}) ContextProperty<T>()[req] = value
shelf_web_socket package Built-in WebSocketUpgrade
Install via CLI
npx skills add https://github.com/serverpod/relic --skill relic-shelf-migration
Repository Details
star Stars 124
call_split Forks 14
navigation Branch main
article Path SKILL.md
More from Creator