Netpress Laravel-inspired backend framework for Node.js
Frameworkv0.1.14 Starterv0.1.12 Docsv1.0.3
Overview Installation Architecture CLI
Architecture

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:

  • config repository
  • 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:

FeatureFirst-use trigger
Mail systemMail.send(...)
Queue systemQueue.dispatch(...) / queue:work CLI
Cache driverscache.get(...) — driver built on first call
File storagestorage.get(...) / storage.put(...)
Rendering engines (React / Vue)first res.render(Component)
Broadcasting / websocketsfirst subscribe or publish
Scheduler internalswhen the scheduler process starts
Optional DB integrationsmodel 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 preload so 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.