Hybrid Bootstrap
Netpress uses a hybrid bootstrap model: a tiny eager core, and lazy providers for everything optional.
Startup stays fast because only the features you actually reach pay their cost — database connections, mail transports, queue workers, cache drivers, and rendering engines are built on first use.
The Two Phases
Every service provider has two lifecycle methods:
import { ServiceProvider } from "@admicaa/netpress";
export default class MyServiceProvider extends ServiceProvider {
// Phase 1 — always runs. Cheap: bind factories into the container.
async register() {}
// Phase 2 — runs for eager providers only. Do startup side effects here.
async boot() {}
}
The kernel calls register() for every provider, then boot() for non-deferred ones. Deferred providers skip phase 2 at startup; their boot() runs when something first resolves their services via app.make(key).
Eager Core
The following run at boot with no opt-out — they form the request/response skeleton:
configrepository- service container
- base request / response layer
- router
- base middleware pipeline
- exception handling
- Laravel-style logger helper
In the starter kit these are owned by AppServiceProvider and RouteServiceProvider.
Lazy On First Use
These are deferred by default. If the feature is never touched, it never loads:
| Feature | First-use trigger |
|---|---|
| Mail system | Mail.send(...) |
| Queue system | Queue.dispatch(...) / queue:work CLI |
| Cache drivers | cache.get(...) — driver built on first call |
| File storage | storage.get(...) / storage.put(...) |
| Rendering engines (React / Vue) | first res.render(Component) |
| Broadcasting / websockets | first subscribe or publish |
| Scheduler internals | when the scheduler process starts |
| Optional DB integrations | model query, DB.connection(), transactions, migrations |
Marking A Provider Deferred
Two static fields on the provider class:
import { ServiceProvider, Mail, config } from "@admicaa/netpress";
import MailService from "../Services/MailService.js";
export default class MailServiceProvider extends ServiceProvider {
static deferred = true;
static provides = ["mail", "mail.transport"];
async register() {
await config.load("mail");
// Factory — nothing expensive runs yet.
this.app.container.bind("mail.transport", () => {
Mail.configure(MailService, { engine: config.get("mail.engine") });
return Mail;
});
this.app.container.bind("mail", () => this.app.container.get("mail.transport"));
}
async boot() {
// Only reached when `await app.make("mail")` is first called.
}
}
Resolving A Deferred Service
From anywhere that holds the Application:
const mail = await app.make("mail");
await mail.send(WelcomeEmail, { to: user.email });
Or — more commonly — just use the facade; it resolves from the container internally and triggers the factory:
import { Mail } from "@admicaa/netpress";
await Mail.send(WelcomeEmail, { to: user.email });
Folder Structure
The hybrid model maps cleanly to the following project layout:
my-app/
├── app/
│ ├── Console/ # artisan commands
│ ├── Http/ # controllers, middleware, requests
│ ├── Models/ # models (lazy-bound to DB)
│ ├── Providers/ # service providers (eager + deferred)
│ │ ├── AppServiceProvider.js
│ │ ├── DatabaseServiceProvider.js
│ │ ├── AuthServiceProvider.js
│ │ ├── MailServiceProvider.js (deferred)
│ │ ├── QueueServiceProvider.js (deferred)
│ │ ├── EventServiceProvider.js
│ │ └── RouteServiceProvider.js
│ ├── Services/ # domain services
│ ├── Mail/ Jobs/ Events/ Listeners/ Observers/ Policies/
│
├── bootstrap/
│ ├── app.js # builds Express + Application kernel
│ └── providers.js # ordered provider list
│
├── config/ # config files (lazy-loaded on demand)
├── database/migrations/
├── database/seeders/
├── resources/views/
├── routes/ # web.js, api.js
├── server.js # `config.loadAll()` → `createApp()` → listen
└── tests/
bootstrap/app.js is the only orchestration code:
import express from "express";
import { Application, config } from "@admicaa/netpress";
import providers from "./providers.js";
import Handler from "../app/Exceptions/Handler.js";
export async function createApp() {
await config.loadAll();
const expressApp = express();
const app = new Application({ express: expressApp });
await app.loadProviders(providers);
expressApp.use(Handler.handle);
expressApp.set("netpress:app", app);
return expressApp;
}
Lazy vs Preloaded Database Connections
The database manager is registered at boot, but no real connection is opened until it is needed. This is the default and matches how models, query builder, transactions, and migrations already behave:
// app/Providers/DatabaseServiceProvider.js
import { ServiceProvider, config, registerApplicationDatabase, ApplicationDatabase } from "@admicaa/netpress";
const db = new ApplicationDatabase({});
export default class DatabaseServiceProvider extends ServiceProvider {
static provides = ["db", "db.manager"];
async register() {
await config.load("database");
const dbConfig = config.get("database");
await registerApplicationDatabase(dbConfig, {
database: db,
preload: dbConfig.preload || [], // empty = fully lazy
});
this.app.container.bind("db", db);
this.app.container.bind("db.manager", db);
}
}
export { db };
Fully-lazy (default)
// config/database.js
export default {
default: process.env.DB_CONNECTION || "mongo",
preload: [],
connections: {
mongo: { uri: process.env.MONGO_URI },
mysql: { host: "...", database: "app", username: "...", password: "..." },
postgres: { host: "...", database: "app", username: "...", password: "..." },
sqlite: { storage: ":memory:", dialect: "sqlite" },
},
};
No socket is opened at boot. The first query triggers the connection:
// User.findOne() opens the mongo connection on demand
const user = await User.findOne({ email });
// DB.connection("analytics") opens that secondary connection on demand
await DB.connection("analytics").table("events").insert({ ... });
Preloading Selected Connections
When you want a fail-fast startup for critical connections:
// config/database.js
export default {
default: process.env.DB_CONNECTION || "mysql",
preload: ["mysql"], // warm only the primary
connections: { /* ... */ },
};
Or via env:
DB_PRELOAD=mysql,analytics node server.js
Each name in preload is opened during DatabaseServiceProvider.register(). Everything else stays lazy.
Single vs Multiple Connections
Both work identically. Single connection:
// Model with no `static connection` uses the default — opens it on first use.
class User extends BaseModel {
static table = "users";
}
Multiple connections:
// Writes to the configured `analytics` connection.
// That connection is opened on the first AuditLog query.
class AuditLog extends BaseModel {
static table = "audit_logs";
static connection = "analytics";
}
Both the facade and individual models funnel through the same lazy registry, so you pay at most one round-trip per logical connection per process.
What Runs When — A Quick Trace
Startup for a typical API request that queries the DB and sends a mail:
server.js
└─ config.loadAll() eager
└─ new Application({ express }) eager
└─ app.loadProviders([...])
├─ AppServiceProvider.register() eager (load cache/storage/view config)
├─ DatabaseServiceProvider.register eager (seed registry, discover models)
├─ AuthServiceProvider.register eager (load auth config)
├─ MailServiceProvider.register eager (bind factory) ← deferred
├─ EventServiceProvider.register eager (register listeners)
├─ RouteServiceProvider.register eager (load view config)
│
├─ AppServiceProvider.boot() eager (mount middleware)
├─ DatabaseServiceProvider.boot() eager (log; no connections)
├─ AuthServiceProvider.boot() eager (mount passport.initialize)
├─ EventServiceProvider.boot() eager
└─ RouteServiceProvider.boot() eager (mount router)
request GET /api/users
└─ controller: await User.find(id)
└─ resolveModelConnection("mongo")
└─ ensureConfiguredConnectionOpen("mongo") ← first use
└─ mongoose.connect(...)
└─ query runs
└─ controller: await Mail.send(Welcome, { to })
└─ container.get("mail.transport") ← first use
└─ MailService transport built, renderer resolved
└─ mail sent
Nothing before the first model query paid a DB cost. Nothing before the first Mail.send() paid a mail cost.
Guidelines
- Put anything that has to mount into Express (middleware, routes) in an eager provider.
- Put anything that requires a socket, subprocess, or filesystem handle behind a factory and mark the provider deferred.
- Preload deliberately — if a connection is required for the app to function at all, add it to
preloadso you fail fast at boot. - Do not guard lazy loads with
if (config.get(...))in the eager register path; the factory itself is the guard.