Skip to content

Application and bootstrap

Everything on this page comes from package:gisila/gisila.dart.

import 'package:gisila/gisila.dart';

The framework entry point. You build one per process (or per isolate), register controllers, services, singletons and middleware on it, then call start().

class GisilaApp {
GisilaApp({AppConfig? config});
final AppConfig config;
final MetricsRegistry metrics;
void use(Middleware middleware);
void registerNamedMiddleware(String name, Middleware middleware);
Middleware namedMiddleware(String name);
void registerService<S extends Service>(ServiceFactory<S> factory);
Service Function()? serviceFactory(Type type);
void registerSingleton<T extends Object>(T instance,
{FutureOr<void> Function(T instance)? dispose});
void registerSingletonFactory<T extends Object>(
FutureOr<T> Function(GisilaApp app) create,
{FutureOr<void> Function(T instance)? dispose});
T singleton<T extends Object>();
void registerController({required ControllerAttacher attacher, String prefix = ''});
void serveStatic(String prefix, String directory,
{StaticFilesConfig config = const StaticFilesConfig()});
void onStart(FutureOr<void> Function() hook);
void onStop(FutureOr<void> Function() hook);
Future<void> init();
Handler buildHandler();
Handler get handler;
Future<HttpServer> start({...});
Future<void> stop({bool force = false, Duration drainTimeout = const Duration(seconds: 30)});
static GisilaApp? maybeOf(Request request);
}

When to use: always. Every gisila server has exactly one GisilaApp per isolate. If you only need a Shelf handler (to mount inside another server, or for tests), still build a GisilaApp and call buildHandler() instead of start().

Constructor parameter Type Default Description
config AppConfig? AppConfig() All framework-wide settings: logging, auth, CORS, rate limits, database, cache and so on.
Property Type Description
config AppConfig The configuration passed to the constructor.
metrics MetricsRegistry The app’s metrics. The built-in HTTP metrics land here when AppConfig.metrics is set; register your own counters, gauges and histograms here too.

Example

import 'package:gisila/gisila.dart';
import 'package:my_api/controllers/users_controller.dart';
import 'package:my_api/services/user_service.dart';
Future<void> main() async {
final app = GisilaApp(
config: AppConfig(
logFormat: LogFormat.json,
cors: const CorsConfig(allowedOrigins: {'https://app.example.com'}),
maxRequestBodyBytes: 1024 * 1024,
healthCheckPath: '/healthz',
),
);
app.registerService<UserService>(UserService.new);
app.registerController(attacher: UsersController().attachToApp);
await app.start(port: 8080, stopOnSignals: true);
}
void use(Middleware middleware)

Adds Shelf middleware to the global pipeline. Middleware registered earlier runs outermost (sees the request first and the response last). User middleware runs inside the framework’s error mapper and database session, so exceptions you throw from it are turned into JSON error responses, and ctx.db<T>() works inside it. See Middleware for the full order.

When to use: cross-cutting behaviour that applies to every request (tenant resolution, tracing, auditing). For behaviour that applies to a few routes, use registerNamedMiddleware with @UseMiddleware, or RouteConfig.middleware.

void registerNamedMiddleware(String name, Middleware middleware)

Registers middleware under a name that @UseMiddleware('name') can refer to. Annotations must be const, so they carry only the name; the real middleware is looked up when the router is built.

Register named middleware before start() / buildHandler(). An annotation that names unregistered middleware throws a StateError while the handler is being built, so misconfiguration fails at startup.

Middleware namedMiddleware(String name)

Returns the middleware registered under name, or throws StateError. Generated code calls this; you rarely need it.

void registerService<S extends Service>(ServiceFactory<S> factory)

Registers a per-request Service factory. The factory runs at most once per request that asks for the service (through a controller parameter, ctx.service<S>() or another service); the instance is attached to the request’s RequestContext and cached for the rest of that request.

app.registerService<UserService>(UserService.new);
// A factory closure can pull in app-wide singletons:
app.registerService<MailService>(() => MailService(app.singleton<SmtpClient>()));

Asking for a service that has no factory throws a StateError, which surfaces as a 500.

Service Function()? serviceFactory(Type type)

Returns the factory registered for type, or null. Used by generated code and tooling.

void registerSingleton<T extends Object>(T instance,
{FutureOr<void> Function(T instance)? dispose})

Registers one app-wide instance shared by every request (an HTTP client, a mailer, a queue client). Read it with singleton<T>(), ctx.singleton<T>() or Service.singleton<T>(). dispose runs on stop(), in reverse registration order.

Throws StateError when called after init() / start() or when T is already registered.

When to use: objects that are expensive to create and safe to share. For anything that holds per-request state, use a Service instead.

void registerSingletonFactory<T extends Object>(
FutureOr<T> Function(GisilaApp app) create,
{FutureOr<void> Function(T instance)? dispose})

Like registerSingleton, but create runs during init() (which start() calls). It may be async (open a connection) and may read singletons registered before it with app.singleton<...>().

// GisilaRedis comes from package:gisila_redis.
app.registerSingletonFactory<GisilaRedis>(
(app) async {
final redis = GisilaRedis.fromUrl(Platform.environment['REDIS_URL']!);
await redis.connect();
return redis;
},
dispose: (redis) => redis.close(),
);

Reading a factory singleton before init() has run throws StateError.

T singleton<T extends Object>()

Returns the singleton registered as T. Throws StateError when none is registered, or when it comes from a factory that has not run yet.

void registerController({required ControllerAttacher attacher, String prefix = ''})

Registers a controller. The code generator emits an attachToApp extension method for every @Controller class whose signature matches ControllerAttacher, so you normally pass its tear-off. Attaching is deferred until the handler is built, so registration order does not matter.

Parameter Type Default Description
attacher ControllerAttacher required Usually MyController().attachToApp.
prefix String '' Extra path prefix for every route of the controller, such as '/api/v1'. Start it with / and leave off the trailing slash.
app
..registerController(attacher: UsersV1().attachToApp, prefix: '/api/v1')
..registerController(attacher: UsersV2().attachToApp, prefix: '/api/v2');
void serveStatic(String prefix, String directory,
{StaticFilesConfig config = const StaticFilesConfig()})

Serves the files under directory at prefix ('/' for a whole frontend, '/assets' and so on). Controller routes are matched first; a path with no file falls through to the next mount and then to the JSON 404. Throws ArgumentError when prefix does not start with /, or when directory does not exist. See StaticFilesConfig.

void onStart(FutureOr<void> Function() hook)

Runs hook during init(), after the singleton factories and before the server accepts connections. Hooks run in registration order; one that throws aborts startup (and tears down what was already set up).

void onStop(FutureOr<void> Function() hook)

Runs hook during stop(), after the server has closed and before singletons are disposed. Hooks run in reverse registration order; one that throws is logged and the rest still run.

Future<void> init()

Connects AppConfig.cache, creates factory singletons and runs onStart hooks. start() calls it for you. Call it yourself when you serve through buildHandler(), for example in tests that use factory singletons or a cache. Runs once; later calls wait for the first. On failure, everything already set up is torn down and the error is rethrown.

Handler buildHandler()

Builds the full Shelf handler: the framework middleware, your middleware, the router with every registered controller, and static mounts. Each call builds a new handler. Use it to embed the app in another Shelf server or to test it (GisilaTestClient calls it for you).

Handler get handler

Alias of buildHandler().

Future<HttpServer> start({
String host = '0.0.0.0',
int port = 8080,
SecurityContext? securityContext,
bool shared = false,
bool stopOnSignals = false,
Duration drainTimeout = const Duration(seconds: 30),
})

Runs init(), builds the handler and serves it with shelf_io. Returns the live HttpServer.

Parameter Type Default Description
host String '0.0.0.0' Interface to bind.
port int 8080 Port to bind.
securityContext SecurityContext? null Serve HTTPS directly. Usually you terminate TLS at a reverse proxy instead.
shared bool false Let several isolates bind the same port; connections are spread between them.
stopOnSignals bool false On SIGINT or SIGTERM (SIGINT only on Windows), call stop(drainTimeout: ...) and then exit(0). Turn this on in containers and under systemd.
drainTimeout Duration 30 seconds How long a signal-triggered stop waits for in-flight requests.
Future<void> stop({bool force = false, Duration drainTimeout = const Duration(seconds: 30)})

Stops accepting connections and waits up to drainTimeout for in-flight requests, then closes whatever is left (force: true closes at once). Then runs onStop hooks, disposes singletons in reverse order, disconnects the cache and calls DatabaseProvider.dispose(). Also tears down an app that was only init()ed. Safe to call when nothing is running.

static GisilaApp? maybeOf(Request request)

Returns the app that is handling request, or null outside a GisilaApp pipeline. Useful in middleware that needs app.config (for example to call the configured authenticator).

See also: Request lifecycle, Services and dependency scopes, Create a project.

Signature of the generated attachToApp method.

typedef ControllerAttacher = void Function(
GisilaApp app,
Router router, {
String prefix,
});

When to use: you pass one to registerController. You only write one by hand if you want to register routes on the router yourself without the generator.

app.registerController(
attacher: (app, router, {prefix = ''}) =>
UsersController().attachToApp(app, router, prefix: prefix),
);
const String kAppKey = 'gisila.app';

The Shelf request.context key holding the active GisilaApp. Prefer GisilaApp.maybeOf(request) over reading it directly.

package:gisila/gisila.dart re-exports the parts of Shelf and shelf_router that controllers and middleware need, so you rarely import them yourself:

Symbol From Notes
Request, Response, Handler, Middleware, Pipeline, HijackException package:shelf Only these names are re-exported from Shelf.
Router and everything else in shelf_router (including the request.params extension) package:shelf_router shelf_router’s own Route class is hidden because it clashes with gisila’s Route annotation.