AdonisJS v7 package that intercepts incoming requests while external services are not yet ready, redirects to a waiting route, and lets you through once they are.
The package handles the detection logic. Rendering the waiting page is entirely up to you — Edge, Inertia, or anything else.
- On boot, the provider starts a background interval that runs all registered checkers
- The middleware intercepts every request — if not ready, redirects to the configured waiting route (or returns
503for non-HTML requests) - Your controller receives
readyandredirect— render however you want, implement polling or SSE as needed
node ace configure @minimalstuff/adonis-waitingThis will:
- Publish
config/waiting.ts - Generate
app/waiting/example_checker.ts - Generate
app/waiting/waiting_controller.ts(stub to fill in) - Register the provider in
adonisrc.ts - Print instructions for middleware and route wiring
Add to the router middleware stack in start/kernel.ts:
router.use([
() => import('@adonisjs/core/bodyparser_middleware'),
() => import('@minimalstuff/adonis-waiting/middleware'),
]);Register one route pointing to your controller in start/routes.ts:
router.get('/waiting', [WaitingController, 'render']);The generated stub at app/waiting/waiting_controller.ts gives you ready and redirect. Implement rendering as you see fit:
// Inertia — pairs with usePolling(2000) on the client
async render({ request, inertia }: HttpContext) {
return inertia.render('waiting/index', {
ready: this.waitingService.isReady(),
redirect: request.input('redirect', '/'),
})
}
// Edge
async render({ request, view }: HttpContext) {
return view.render('waiting/index', {
ready: this.waitingService.isReady(),
redirect: request.input('redirect', '/'),
})
}// resources/pages/waiting/index.tsx
import { router, usePage } from '@inertiajs/react';
import { useEffect } from 'react';
export default function WaitingPage() {
const { ready, redirect } = usePage<{ ready: boolean; redirect: string }>()
.props;
useEffect(() => {
if (ready) router.visit(redirect);
}, [ready, redirect]);
usePolling(2000);
return <p>Starting up…</p>;
}A checker is any class that implements ServiceChecker:
import type { ServiceChecker } from '@minimalstuff/adonis-waiting';
export class RedisChecker implements ServiceChecker {
readonly name = 'redis';
readonly description = 'Redis connection'; // optional, surfaced by getCheckerStatuses()
async check(): Promise<boolean> {
try {
await redis.ping();
return true;
} catch {
return false;
}
}
}The check() method is called every checkInterval ms. Return true when ready, false otherwise. Thrown exceptions are treated as false.
A checker can wrap anything: a database connection, an HTTP health endpoint, a file on disk, a CLI command — anything expressible as an async boolean.
// config/waiting.ts
import { defineConfig } from '@minimalstuff/adonis-waiting';
import { RedisChecker } from '#waiting/redis_checker';
import { DatabaseChecker } from '#waiting/database_checker';
export default defineConfig({
checkers: [new RedisChecker(), new DatabaseChecker()],
checkInterval: 2_000, // ms between each check cycle, default: 2000
waitingRoute: '/waiting', // route the middleware redirects to, default: '/waiting'
});All checkers must return true in the same cycle for the app to be considered ready.
interface ServiceChecker {
readonly name: string;
readonly description?: string; // shown by getCheckerStatuses(), purely informational
readonly enabled?: boolean;
check(): Promise<boolean>;
}Type-safe config helper.
Singleton registered in the container. Inject it wherever you need to check readiness state.
import { inject } from '@adonisjs/core';
import { WaitingService } from '@minimalstuff/adonis-waiting';
@inject()
export class WaitingController {
constructor(private readonly waitingService: WaitingService) {}
}| Method | Returns | Description |
|---|---|---|
isReady() |
boolean |
Whether all checkers passed on the last cycle |
getWaitingRoute() |
string |
Configured waiting route |
getCheckInterval() |
number |
Configured check interval in ms |
getCheckerStatuses() |
CheckerStatus[] |
Per-checker status, in config order |
Returned by getCheckerStatuses() — one entry per configured checker, useful for rendering a status list on the waiting page.
type CheckerStatus =
| { name: string; description?: string; state: 'disabled' }
| { name: string; description?: string; state: 'pending' }
| {
name: string;
description?: string;
state: 'healthy';
lastCheckedAt: Date;
}
| {
name: string;
description?: string;
state: 'unhealthy';
lastCheckedAt: Date;
}
| {
name: string;
description?: string;
state: 'error';
lastCheckedAt: Date;
error: string;
};disabled— checker hasenabled: false, never runpending— checker hasn't completed a first check cycle yethealthy/unhealthy— lastcheck()resolvedtrue/falseerror— lastcheck()threw or rejected;errorholds the message
const checkers = waitingService.getCheckerStatuses();
// [{ name: 'redis', description: 'Redis connection', state: 'healthy', lastCheckedAt: ... }, ...]Router middleware. Intercepts requests when not ready:
- HTML requests → redirect to
{waitingRoute}?redirect={originalUrl} - Non-HTML requests →
503 { error: 'E_SERVICE_UNAVAILABLE', retryAfter: 2 }
- If no checkers are configured, the service is considered ready immediately
- The first check runs at boot — there is a brief window before the first cycle completes where
isReady()returnsfalse - The middleware does not redirect requests whose URL starts with the configured
waitingRoute(avoids redirect loops) - The
redirectquery parameter is your responsibility to sanitise in the controller before passing to the renderer