Launchpad 🚀

The missing dependency injection for Serverpod. Let your pod launch with everything onboard!

Launchpad gives every endpoint method, streaming method and future call its own request-scoped get_it container, and a server context for dependencies shared across all requests.

Beta: the API may still change before 1.0.

Installation

dart pub add launchpad get_it

Launchpad works great with injectable, but it is optional.

Getting started

1. Register request-scoped dependencies

Set Launchpad.launchpadInit in your server's main function. It is called with a fresh GetIt container for every request. If you use injectable, pass the generated getIt.init() function:

// with injectable
Launchpad.launchpadInit = (getIt) => getIt.init();

// or without
Launchpad.launchpadInit = (getIt) {
  getIt.registerFactory<HelloUseCase>(() => HelloUseCase());
};

The current Session is always registered in the request container, so your dependencies can depend on it.

2. Wrap your endpoint methods

Wrap every endpoint method that returns a Future with Launchpad.launch:

class HelloEndpoint extends Endpoint {
  Future<String> hello(Session session, String name) => Launchpad.launch(
    session,
    (context) async {
      final useCase = context.get<HelloUseCase>();
      return useCase(name: name);
    },
  );
}

Need different dependencies for one method? Pass initDependencies to replace launchpadInit for that call:

Launchpad.launch(
  session,
  (context) async => context.get<SpecialService>().run(),
  initDependencies: (getIt) {
    getIt.registerFactory<SpecialService>(() => SpecialService());
  },
);

Streaming methods

Serverpod endpoint methods can return a Stream. Launchpad has two ways to wrap them, Launchpad.launchStream and Launchpad.launchStreamSync. Both take the same arguments as Launchpad.launch, including initDependencies.

Launchpad.launchStream: generator handlers

Use it when the handler is an async* generator. Dependencies are initialized when the stream is listened to, and the request context stays available for the whole lifetime of the stream:

class HelloEndpoint extends Endpoint {
  Stream<String> countdown(Session session, int from) =>
      Launchpad.launchStream(session, (context) async* {
        final serverState = context.serverContext.get<ServerState>();

        for (var i = from; i > 0; i--) {
          yield '$i... (visitors so far: ${serverState.counter})';
          await Future.delayed(const Duration(seconds: 1));
        }
        yield 'Liftoff!';
      });
}

Launchpad.launchStreamSync: returning an existing stream

Use it when the handler already has a stream to return and doesn't need a generator. Dependencies are initialized immediately and the handler's stream is returned as is:

class ChatEndpoint extends Endpoint {
  Stream<ChatMessage> messages(Session session, String channel) =>
      Launchpad.launchStreamSync(session, (context) {
        final chatService = context.get<ChatService>();
        return chatService.messages(channel);
      });
}

Which one to use

launchStream launchStreamSync
Handler async* generator (or any stream) Plain function returning a stream
Dependencies are set up When the stream is listened to Immediately, when the method is called
Returned stream Wrapped, always single-subscription The handler's stream, unchanged
Errors in the handler Delivered as stream errors Thrown synchronously
Cancelling the stream Cancels the handler's stream Cancels the handler's stream

For Serverpod endpoints both behave the same towards the client: Serverpod sends errors thrown while calling a streaming method to the client as stream errors.

Methods that only take Stream parameters but return a Future keep using Launchpad.launch.

Future calls

Future call methods receive a regular Session, so wrap them with Launchpad.launch just like endpoint methods:

class VisitorReportFutureCall extends FutureCall {
  Future<void> report(Session session, String reason) => Launchpad.launch(
    session,
    (context) async {
      final serverState = context.serverContext.get<ServerState>();
      session.log('[$reason] Visitors so far: ${serverState.counter}');
    },
  );
}

After running serverpod generate, schedule it as usual (after pod.start()):

await pod.futureCalls
    .callWithDelay(const Duration(seconds: 10))
    .visitorReport
    .report('startup');

Server context

Share dependencies and their state across all requests through the server context, for example database drivers or connection pools.

First, register your shared dependencies before pod.start():

void run(List<String> args) async {
  final pod = Serverpod(args, Protocol(), Endpoints());

  Launchpad.initServerContext(pod, (getIt) {
    // e.g. getIt.registerSingleton<YourService>(YourServiceImplementation());
    getIt.registerSingleton(ServerState());
  });

  await pod.start();
}

Then access them through context.serverContext:

class HelloEndpoint extends Endpoint {
  Future<String> hello(Session session) => Launchpad.launch(
    session,
    (context) async {
      final serverState = context.serverContext.get<ServerState>();

      serverState.counter += 1;

      return 'Hello from Serverpod! You are visitor number ${serverState.counter}.';
    },
  );
}

class ServerState {
  int counter = 0;
}

Result: the state is shared between requests:

flutter: Server responded: Hello from Serverpod! You are visitor number 17.
flutter: Server responded: Hello from Serverpod! You are visitor number 18.
flutter: Server responded: Hello from Serverpod! You are visitor number 19.

Warning: the server context lives in memory of a single pod. It is not shared between your server instances, so this counter is not a good way to share state in production. Use Redis or the database for state that must be shared across pods.

API reference

API Purpose
Launchpad.launchpadInit Registers request-scoped dependencies for every request
Launchpad.initServerContext(pod, init) Registers server-wide dependencies once at startup
Launchpad.launch(session, handler, {initDependencies}) Wraps endpoint methods returning a Future, and future calls
Launchpad.launchStream(session, handler, {initDependencies}) Wraps streaming methods with an async* handler
Launchpad.launchStreamSync(session, handler, {initDependencies}) Wraps streaming methods that return an existing stream as is
LaunchpadRequestContext.get<T>() Resolves a request-scoped dependency
LaunchpadRequestContext.serverContext Accesses the server-wide dependencies
LaunchpadServerContext.get<T>() Resolves a server-wide dependency
session.launchpad The request-scoped GetIt container of a session

Example

A complete Serverpod 4 project (server, client and Flutter app) using endpoints, a streaming method and a future call lives in the example directory.

Libraries

launchpad
Dependency injection for Serverpod endpoints, streaming methods and future calls, built on get_it.