npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nitronjs/framework

v0.5.5

Published

NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React int

Readme


Table of Contents


Quick Start

npx -y @nitronjs/framework my-app
cd my-app
npm run storage:link
npm run dev

Your app will be running at http://localhost:3000


Core Concepts

Server Components (Default)

Every .tsx file in resources/views/ is a Server Component by default. They run on the server and have full access to your database and file system.

// resources/views/Site/Home.tsx
import User from '@models/User';

export default async function Home() {
    const users = await User.get();

    return (
        <div>
            <h1>Users</h1>
            <ul>
                {users.map(user => (
                    <li key={user.id}>{user.name}</li>
                ))}
            </ul>
        </div>
    );
}

Client Components

Add "use client" at the top of a file to make it interactive. These components hydrate on the browser and can use React hooks.

"use client";
import { useState } from 'react';

export default function Counter() {
    const [count, setCount] = useState(0);

    return (
        <button onClick={() => setCount(count + 1)}>
            Count: {count}
        </button>
    );
}

Client components cannot import server-only modules (the database, DateTime, etc.). Pass server data down as props from a Server Component instead.

Layouts

Create Layout.tsx files to wrap pages. Layouts are discovered automatically by walking up the directory tree. During SPA navigation, layouts persist and only page content is updated — no full page reloads.

resources/views/
├── Layout.tsx             # Root layout (wraps everything)
├── Site/
│   └── Home.tsx           # Uses root Layout
└── Admin/
    ├── Layout.tsx         # Admin layout (nested inside root)
    └── Dashboard.tsx      # Uses both layouts

Routing

Define routes in routes/web.js:

import { Route } from '@nitronjs/framework';
import HomeController from '../app/Controllers/HomeController.js';
import UserController from '../app/Controllers/UserController.js';

// Basic routes
Route.get('/', HomeController.index).name('home');
Route.get('/about', HomeController.about).name('about');

// Route parameters
Route.get('/users/:id', UserController.show).name('user.show');

// RESTful routes
Route.post('/users', UserController.store).name('user.store');
Route.put('/users/:id', UserController.update).name('user.update');
Route.delete('/users/:id', UserController.destroy).name('user.destroy');

// Route groups with prefix, name prefix, and middleware
Route.prefix('/admin').name('admin.').middleware('auth').group(() => {
    Route.get('/', DashboardController.index).name('dashboard');
    Route.get('/users', AdminUserController.index).name('users');

    // Nested groups
    Route.prefix('/pages').name('pages.').group(() => {
        Route.get('/:id/edit', PagesController.edit).name('edit');
    });
});

// Middleware-only groups
Route.middleware('guest').group(() => {
    Route.get('/login', AuthController.getLogin).name('login');
    Route.post('/login', AuthController.postLogin);
});

CSRF protection is applied automatically to the HTTP methods configured in config/session.js — you do not add it to routes manually.

URL Generation

The global route() function is available everywhere — controllers, views, and client-side code:

// Basic
route('home')                                        // => "/"

// With parameters
route('user.show', { id: 1 })                        // => "/users/1"

// With query string
route('admin.users', {}, { page: 2, q: 'search' })   // => "/admin/users?page=2&q=search"

// Parameters + query string
route('admin.pages.edit', { id: 5 }, { tab: 'seo' }) // => "/admin/pages/5/edit?tab=seo"

Controllers

Controllers are plain classes with static async route handlers. Each handler receives req and res.

// app/Controllers/UserController.js
import User from '../Models/User.js';

class UserController {
    static async index(req, res) {
        const users = await User.get();

        return res.view('User/Index', {
            users
        });
    }

    static async show(req, res) {
        const user = await User.find(req.params.id);

        if (!user) {
            return res.code(404).view('Errors/NotFound');
        }

        return res.view('User/Show', {
            user
        });
    }

    static async store(req, res) {
        const user = new User();
        user.name = req.body.name;
        user.email = req.body.email;
        await user.save();

        return res.redirect(route('user.show', { id: user.id }));
    }
}

export default UserController;

Path Aliases

NitronJS provides built-in path aliases for clean imports in .tsx view files:

| Alias | Path | |---|---| | @models/* | app/Models/* | | @controllers/* | app/Controllers/* | | @middlewares/* | app/Middlewares/* | | @views/* | resources/views/* | | @css/* | resources/css/* | | @/* | Project root |


Request & Response

The req object

req is the Fastify request, with req.session and req.auth added by the framework.

| Property / Method | Description | |---|---| | req.params | Route parameters (/users/:idreq.params.id) | | req.body | Parsed request body | | req.query | Query string parameters | | req.headers | Request headers | | req.cookies | Parsed cookies | | req.ip | Client IP address | | req.url / req.method | Request URL and HTTP method | | req.isMultipart() | true for multipart/form-data requests (file uploads) | | req.locale | Current locale (set by the i18n layer) | | req.session | Session instance — see Sessions | | req.auth | Authentication helper — see Authentication |

The res object

res is the Fastify reply, with res.view() added by the framework.

| Method | Description | |---|---| | res.view(name, params = {}) | Render a React Server Component view as HTML. Throws 404 if the view does not exist. | | res.send(payload) | Send a response. Objects/arrays are auto-serialized as JSON. | | res.code(statusCode) | Set the HTTP status code (chainable). | | res.redirect(url) | Redirect the client. | | res.type(contentType) | Set the Content-Type header. | | res.header(key, value) | Set a response header. | | res.setCookie(name, value, options) | Set a cookie. | | res.sendFile(path) | Stream a file as the response. |

// HTML view
return res.view('User/Show', { user });

// JSON (object is auto-serialized)
return res.send({ status: 'success', data: user });

// JSON with a status code
return res.code(422).send({ errors: validation.errors() });

// Redirect
return res.redirect(route('home'));

Use res.code(...) to set the status — there is no res.status(). To return JSON, just pass an object to res.send(...) — there is no res.json().


Database

Models

A model is a class extending Model with a static table property. That is the only required configuration — the primary key is always id.

import { Model } from '@nitronjs/framework';

class User extends Model {
    static table = 'users';
}

export default User;

Query Builder

Static query methods are available directly on the model.

// Fetch
const users = await User.get();
const user = await User.find(1);
const first = await User.where('email', '[email protected]').first();

// Where clauses
await User.where('role', 'admin').get();
await User.where('age', '>=', 18).get();
await User.where({ role: 'admin', active: 1 }).get();
await User.orWhere('role', 'editor').get();
await User.whereIn('id', [1, 2, 3]).get();
await User.whereNotIn('status', ['banned']).get();
await User.whereBetween('age', [18, 65]).get();
await User.whereNot('role', 'guest').get();

// Selecting, ordering, paginating
await User
    .select('id', 'name', 'email')
    .where('active', 1)
    .orderBy('created_at', 'desc')
    .limit(10)
    .offset(20)
    .get();

// Joins & grouping
await User
    .join('posts', 'users.id', '=', 'posts.user_id')
    .groupBy('users.id')
    .get();

// Aggregates
const total = await User.count();
const distinct = await User.countDistinct('email');
const maxAge = await User.max('age');
const minAge = await User.min('age');
const sumScore = await User.sum('score');
const avgScore = await User.avg('score');

// Raw expressions
await User.selectRaw('COUNT(*) as total, role').groupBy('role').get();

// Create
const user = new User();
user.name = 'John';
user.email = '[email protected]';
await user.save();

// Update
await User.where('id', 1).update({ name: 'Jane' });

// Delete
await User.where('id', 1).delete();

// Serialize an instance to a plain object
const data = user.toJSON();

DB Facade

For queries that don't map cleanly to a single model, use the DB facade directly.

import { DB } from '@nitronjs/framework';

// Query builder against any table
const rows = await DB.table('users').where('active', 1).get();

// Raw SQL with bound parameters
const result = await DB.rawQuery('SELECT * FROM users WHERE id = ?', [1]);

// Raw expression inside a query builder chain
await DB.table('users').select(DB.rawExpr('COUNT(*) as total')).first();

Transactions

DB.transaction() runs a callback inside a database transaction. It commits on success and rolls back if the callback throws.

import { DB } from '@nitronjs/framework';

await DB.transaction(async (trx) => {
    await trx.table('accounts').where('id', 1).update({ balance: 900 });
    await trx.table('accounts').where('id', 2).update({ balance: 1100 });
    await trx.rawQuery('INSERT INTO transfers (amount) VALUES (?)', [100]);
});

The trx object exposes table(), rawQuery(), query(), and execute() — all scoped to the transaction's connection. A transaction has a 30-second default timeout.

Migrations

npm run make:migration create_posts_table

Migrations live in database/migrations/ and use the Schema builder:

import { Schema } from '@nitronjs/framework';

class CreatePostsTable {
    static async up() {
        await Schema.create('posts', (table) => {
            table.id();
            table.string('title');
            table.text('body');
            table.string('slug').unique();
            table.boolean('published').default(false);
            table.json('metadata').nullable();
            table.foreign('user_id').references('id').on('users').onDelete('CASCADE');
            table.timestamp('created_at');
            table.timestamp('updated_at').nullable();
        });
    }

    static async down() {
        await Schema.dropIfExists('posts');
    }
}

export default CreatePostsTable;

Schema Builder Reference

Column types: id(), increments(), bigIncrements(), integer(), bigInteger(), tinyInteger(), smallInteger(), mediumInteger(), float(), double(), decimal(), boolean(), string(), text(), mediumText(), longText(), json(), jsonb(), date(), dateTime(), timestamp(), timestamps(), softDeletes(), binary(), uuid(), enum()

Column modifiers (chainable): .nullable(), .default(value), .unique(), .primary(), .index(), .unsigned(), .autoIncrement(), .comment(text), .after(column), .first(), .change()

Indexes & keys: table.index(columns), table.unique(columns), table.primary(columns), table.foreign(column).references(col).on(table).onDelete(action).onUpdate(action)

Altering tables: table.dropColumn(name), table.dropColumns(...names), table.renameColumn(from, to)

Schema methods: Schema.create(), Schema.table() (alter), Schema.drop(), Schema.dropIfExists(), Schema.rename(), Schema.hasTable(), Schema.hasColumn()

npm run migrate              # Run pending migrations
npm run migrate:safe         # Run migrations after an automatic DB backup
npm run migrate:fresh        # Drop all tables and re-run
npm run migrate:fresh:seed   # Drop, migrate, and seed
npm run migrate:rollback     # Rollback last batch
npm run migrate:status       # Show migration status

Seeders

npm run make:seeder UserSeeder

Seeders live in database/seeders/. Each seeder is a class with a static async run() method:

import { Hash } from '@nitronjs/framework';
import User from '../../app/Models/User.js';

class UserSeeder {
    static async run() {
        const user = new User();
        user.name = 'Admin';
        user.email = '[email protected]';
        user.password = await Hash.make('password');
        await user.save();
    }
}

export default UserSeeder;

Run seeders with npm run seed (runs every seeder, alphabetically) or npx njs seed UserSeeder (one seeder). Seeders can call other seeders by importing them and calling .run() directly.


Authentication

Login & Logout

req.auth provides authentication scoped to the default guard.

class AuthController {
    static async postLogin(req, res) {
        const success = await req.auth.attempt({
            email: req.body.email,
            password: req.body.password
        });

        if (!success) {
            return res.view('Auth/Login', {
                error: 'Invalid credentials'
            });
        }

        return req.auth.home();
    }

    static async logout(req, res) {
        await req.auth.logout();

        return res.redirect(route('home'));
    }
}

| Method | Description | |---|---| | await req.auth.attempt(credentials) | Validate credentials and log the user in. Returns boolean. | | await req.auth.user() | The authenticated user, or null. | | req.auth.check() | true if a user is authenticated (synchronous). | | await req.auth.logout() | End the session. | | req.auth.home() | Redirect to the guard's configured home route. | | req.auth.redirect() | Redirect to the guard's configured redirect route. | | req.auth.guard(name) | Same API, scoped to a named guard. |

Guards

Guards let one app authenticate multiple user types (e.g. user and admin). They are defined in config/auth.js:

import User from '../app/Models/User.js';
import Admin from '../app/Models/Admin.js';

export default {
    defaults: {
        guard: 'user'
    },
    guards: {
        user: {
            provider: User,
            identifier: 'email',
            home: 'dashboard',
            redirect: 'login'
        },
        admin: {
            provider: Admin,
            identifier: 'username',
            home: 'admin.dashboard',
            redirect: 'admin.login'
        }
    }
};
// Use a specific guard
await req.auth.guard('admin').attempt({ username, password });
req.auth.guard('admin').check();
const admin = await req.auth.guard('admin').user();

Multi-Factor Authentication (MFA)

Each guard exposes a TOTP-based MFA helper at req.auth.mfa. The user's model must have an mfa column (the encrypted secret is stored there).

// 1. Start setup — returns a QR code to show the user
const { qrCode, secret, otpauthUri } = await req.auth.mfa.generate({
    issuer: 'My App',
    label: user.email
});

// 2. Confirm setup with a code from the user's authenticator app
const result = await req.auth.mfa.confirmSetup(req.body.code);
if (result.success) {
    // result.recoveryCodes — 8 single-use codes, show them once
}

// 3. At login time, after password check
const valid = await req.auth.mfa.verify(req.body.code);
const viaRecovery = await req.auth.mfa.verifyRecoveryCode(req.body.code);

// State helpers
await req.auth.mfa.enabled();   // boolean — MFA confirmed for this user
await req.auth.mfa.disable();   // clear MFA data (verify the password yourself first)
req.auth.mfa.isPending();       // session flag: password OK, awaiting MFA code
req.auth.mfa.setPending();
req.auth.mfa.clearPending();

Sessions

req.session is a per-request session instance.

// Read & write
req.session.set('cart_id', 42);
const cartId = req.session.get('cart_id');
const all = req.session.all();

// Flash messages — available only on the next request
req.session.flash('success', 'Profile updated!');
const message = req.session.getFlash('success');

// CSRF tokens
const token = req.session.getCsrfToken();          // current token (generates one if absent)
req.session.generateCsrfToken();                   // force a new token
req.session.verifyCsrfToken(submittedToken);       // timing-safe comparison

// Regenerate the session ID (anti-fixation, e.g. after login)
req.session.regenerate();

// Getters
req.session.id;
req.session.createdAt;

The session driver (none, file, memory, or redis) is configured in config/session.js.


Validation

import { Validator } from '@nitronjs/framework';

const validation = Validator.make(req.body, {
    name: 'required|string|min:2|max:100',
    email: 'required|email',
    password: 'required|string|min:8|confirmed',
    age: 'numeric|min:18',
    avatar: 'file|mimes:png,jpg|max:2097152'
});

if (validation.fails()) {
    return res.code(422).send({
        errors: validation.errors()
    });
}

const clean = validation.validated();

| Method | Description | |---|---| | Validator.make(data, rules) | Build a validator. | | validation.fails() / validation.passes() | Whether validation failed/passed. | | validation.errors() | Error messages keyed by field. | | validation.validated() | The validated subset of the input data. |


Middleware

Middleware is a class with a static async handler(req, res) method. Returning a response (res.send, res.redirect, res.view, or an auth redirect) halts the chain; returning nothing lets it continue.

// app/Middlewares/CheckAge.js
class CheckAge {
    static async handler(req, res) {
        if (req.query.age < 18) {
            return res.code(403).send('Access denied');
        }
    }
}

export default CheckAge;

Register middleware aliases in app/Kernel.js:

import Authentication from './Middlewares/Authentication.js';
import CheckAge from './Middlewares/CheckAge.js';

export default {
    routeMiddlewares: {
        'auth': Authentication,
        'check-age': CheckAge
    }
};

Reference middleware by its string alias in routes:

Route.get('/admin', AdminController.index).middleware('auth');

Middleware can receive a parameter — the framework's auth middleware, for example, accepts a guard name: static async handler(req, res, guardName = 'user').


Real-Time (Socket.IO)

NitronJS ships with first-class Socket.IO support. The API mirrors HTTP routing exactly — same prefix/middleware/name/group chain, same middleware aliases — so you write WebSocket endpoints in the same language as REST endpoints.

Declaring routes

// routes/web.js
import { Route } from '@nitronjs/framework';
import Game from '../app/Sockets/Game.js';

Route.io.prefix('/game').middleware('auth').name('game.').group(() => {
    Route.io.on('connect', Game.onConnect);

    Route.io.on('room.join', Game.onRoomJoin)
        .middleware('game.room.join.form')
        .name('room.join');

    Route.io.on('round.guess', Game.onRoundGuess)
        .middleware('game.round.guess.form')
        .name('round.guess');

    Route.io.on('disconnect', Game.onDisconnect);
});

| HTTP | Socket.IO equivalent | |---|---| | Route.get(path, h) | Route.io.on(event, h) | | Route.prefix('/admin') | Route.io.prefix('/game') → Socket.IO namespace | | .middleware('auth') | Same — runs at handshake for namespace, per-event for Route.io.on().middleware() | | .name('admin.') | Same — produces names like game.room.join | | .group(callback) | Same |

Handler classes

Place handler classes in app/Sockets/. Methods follow an on<EventName> convention parallel to controllers' getX/postX:

// app/Sockets/Game.js
class Game {

    static async onConnect (socket) {
        // socket.data.user and socket.data.user_id are populated when the
        // namespace uses the "auth" middleware — identity is ready for every
        // subsequent event handler.
    }

    static async onRoomJoin (socket, payload, ack) {
        // payload was validated by the per-event Form middleware before reaching here
        socket.join(payload.code);
        socket.data.room_code = payload.code;

        ack({ status: 'success', data: { code: payload.code } });
        socket.to(payload.code).emit('room.member_joined', {
            user_id: socket.data.user_id
        });
    }

    static async onRoundGuess (socket, payload, ack) {
        // ... game logic
        ack({ status: 'success', data: { colors, attempts_left, won } });
        socket.to(socket.data.room_code).emit('round.opponent_progress', {
            user_id: socket.data.user_id,
            attempts_left
        });
    }

    static async onDisconnect (socket) {
        // cleanup
    }

}

export default Game;

Signatures:

  • connect / disconnect(socket). No middleware runs (lifecycle events).
  • All other events → (socket, payload, ack). payload is whatever the client emitted, ack is the optional acknowledgement callback the client passed.

Form middleware for events

Per-event validation works the same as HTTP — write a Form middleware class, register it as an alias in Kernel.js, attach it via .middleware(). Only the signature differs (no req/res; socket/payload/ack):

// app/Middlewares/Game_Round_Guess_Form.js
import { Validator } from '@nitronjs/framework';

class Game_Round_Guess_Form {
    static async handler (socket, payload, ack) {
        const validate = Validator.make(payload, {
            word: 'required|string|min:5|max:5'
        });
        if (validate.fails()) {
            ack({ status: 'failed', message: 'A valid 5-letter word is required.' });
            return false;  // halt the chain, handler not called
        }
    }
}

export default Game_Round_Guess_Form;

Returning false halts the chain. Anything else (including undefined) lets the handler run.

Namespace middleware (the one attached at Route.io.prefix(...).middleware(...)) runs once during handshake. The same HTTP middleware classes (Authentication, Guest, your own) work without modification — the framework adapter converts the (req, res) signature into Socket.IO's handshake context. If your middleware sends a response or redirects, the framework rejects the handshake.

Identity (socket.data.user)

When a namespace's middleware chain includes the auth alias and the user is authenticated, the framework attaches the loaded User model:

socket.data.user      // User model instance
socket.data.user_id   // Convenience: user.id

This happens once at handshake, so event handlers don't need to re-resolve the user on every emit.

Native Socket.IO API

Inside handlers, all of Socket.IO's native API is available unchanged:

socket.join(roomCode);                              // join a room
socket.leave(roomCode);                             // leave
socket.to(roomCode).emit('event', { ... });         // broadcast to room (excludes sender)
socket.broadcast.emit('event', { ... });            // broadcast to namespace (excludes sender)
socket.emit('event', { ... });                      // send to this socket only
socket.data.x = 'value';                            // per-connection state
socket.rooms;                                       // Set of rooms this socket is in
socket.disconnect(true);                            // force close

Push from outside a handler — Sockets helper

Use Sockets for handler-free pushes (timers, background jobs, controllers):

import { Sockets } from '@nitronjs/framework';

Sockets.broadcast('/game', 'ABC234', 'round.started', { round_number: 3 });

// Or get the namespace and use Socket.IO's API directly
const ns = Sockets.namespace('/game');
ns.to('ABC234').emit('round.ended', { winner_id: 42 });

// Inspect — list connected sockets in a room
const sockets = await Sockets.in('/game', 'ABC234').fetchSockets();
console.log(`${sockets.length} player(s) online`);

Client-side

Use the official socket.io-client. The server is websocket-only (HTTP long-polling is not supported with the Fastify attachment), so clients must pass transports: ["websocket"] — otherwise the default polling handshake 404s and the connection never establishes:

import { io } from 'socket.io-client';

const socket = io('/game', { withCredentials: true, transports: ['websocket'] });

socket.on('connect', () => {
    socket.emit('room.join', { code: 'ABC234' }, (resp) => {
        console.log(resp);  // { status: 'success', data: { code: 'ABC234' } }
    });
});

socket.on('round.opponent_progress', ({ user_id, attempts_left }) => {
    // ... update UI
});

// Acknowledgement = synchronous request/response over WebSocket
const resp = await new Promise(resolve =>
    socket.emit('round.guess', { word: 'kalem' }, resolve)
);

HMR

In development, changes to files under app/Sockets/ and to any middleware they use are picked up on the next event reception — no server restart. Existing connections stay open; only the dispatched handler is re-imported from disk. This works for event handlers and event middleware; namespace middleware changes (the ones declared at Route.io.prefix().middleware() time) still require routes/web.js to reload, which the dev server handles automatically.

Backwards compatibility

If your app never calls Route.io.on(), the Socket.IO server is not booted. No port is opened, no memory is held, no behavior changes. WebSocket support is fully opt-in.


Localization (i18n)

Translation files live in resources/langs/. Each locale is either a single JSON file (tr.json) or a folder of namespaced files (tr/messages.json).

// resources/langs/en.json
{
    "welcome": "Welcome",
    "greeting": "Hello :name",
    "cart": {
        "items": "{count} item|{count} items"
    }
}

Translate with the global __() function (or its alias lang()) — available in views, controllers, and client code:

__('welcome')                          // "Welcome"
__('greeting', { name: 'Burak' })       // "Hello Burak"
__('cart.items', { count: 1 })          // "1 item"
__('cart.items', { count: 5 })          // "5 items"

Read the current request locale with the global locale() function — available everywhere (controllers, middleware, server views, and client-side code). It is hydration-safe: the value is identical on the server and after client hydration.

locale()                                      // "tr"

// Pass the current locale to a localized route (e.g. routes under /:locale)
route('cms.dashboard', { locale: locale() })  // => "/tr/cms"
<html lang={locale()}>

The Lang class is available for server-side use:

import { Lang } from '@nitronjs/framework';

Lang.get('greeting', { name: 'Burak' });
Lang.has('welcome');                    // boolean
Lang.locale();                          // current locale
Lang.setLocale('tr');                   // change locale for the current request
  • Dot notation drills into nested keys: cart.items.
  • A {count} parameter triggers pluralization on |-separated values.
  • :param and {param} placeholders are both replaced from the params object.
  • An unresolved key returns the key itself, so missing translations are obvious.
  • In dev mode, language files hot-reload on change.

Mail

import { Mail } from '@nitronjs/framework';

await Mail.from('[email protected]')
    .to('[email protected]')
    .subject('Welcome!')
    .html('<h1>Hello</h1>')
    .attachment({ filename: 'file.pdf', path: '/path/to/file.pdf' })
    .send();

// Using a view as the email body
await Mail.to('[email protected]')
    .subject('Welcome!')
    .view('emails/welcome', { name: 'Alice' })
    .send();

SMTP credentials are read from the MAIL_* environment variables.


Storage

import { Storage } from '@nitronjs/framework';

await Storage.put(file, 'upload_files', 'image.jpg');
const buffer = await Storage.get('upload_files/image.jpg');
await Storage.delete('upload_files/old.jpg');
await Storage.move('upload_files/a.jpg', 'upload_files/b.jpg');
Storage.exists('upload_files/image.jpg');
Storage.url('upload_files/image.jpg');  // => "/storage/upload_files/image.jpg"

Pass true as the last argument to get/put/delete/move/exists to operate on private storage (storage/app/private/), which is not web-accessible.


Views (Manual Rendering)

Controllers normally render with res.view(). To render a view to an HTML string yourself — for emails, previews, or background jobs — use the View class:

import { View } from '@nitronjs/framework';

const html = await View.render('emails/Receipt', { order });
const exists = View.exists('emails/Receipt');

Utilities

Encryption

import { AES } from '@nitronjs/framework';

const token = AES.encrypt({ userId: 1, expires: '2025-12-31' });
const data = AES.decrypt(token);  // Returns false on tamper

Hashing

import { Hash } from '@nitronjs/framework';

const hashed = await Hash.make('password123');
const valid = await Hash.check('password123', hashed);

The bcrypt cost factor is configured in config/hash.js.

Logging

import { Log } from '@nitronjs/framework';

Log.info('User registered', { userId: 1 });
Log.error('Payment failed', { orderId: 123 });
Log.warning('Slow query', { ms: 1200 });
Log.debug('Query executed', { sql: '...' });

DateTime

Use DateTime for all server-side date and time work — never new Date().

import { DateTime } from '@nitronjs/framework';

DateTime.toSQL();                          // "2026-05-14 10:30:00" (now, SQL format)
DateTime.toSQL(timestamp);                 // a specific timestamp in SQL format
DateTime.getDate(timestamp, 'Y-m-d H:i');  // formatted date string
DateTime.getTime(sqlDateTime);             // SQL datetime → millisecond timestamp
DateTime.addDays(7);                       // 7 days from now, SQL format
DateTime.addHours(3);
DateTime.addMinutes(30);
DateTime.subDays(7);
DateTime.subHours(2);
DateTime.subMinutes(15);

Faker

Built-in fake data generator for seeders and testing:

import { Faker } from '@nitronjs/framework';

Faker.fullName();              // "John Smith"
Faker.email();                 // "[email protected]"
Faker.phoneNumber();           // "+1 555 0142"
Faker.sentence();              // "Lorem ipsum dolor sit amet."
Faker.paragraph();             // multi-sentence text
Faker.int(1, 100);             // 42
Faker.float(0, 1, 2);          // 0.37
Faker.boolean();               // true
Faker.uuid();                  // "550e8400-e29b-41d4-a716-446655440000"
Faker.creditCard();            // Luhn-valid card number
Faker.hexColor();              // "#a3f29c"
Faker.city();                  // "Istanbul"
Faker.companyName();           // "Acme Inc."
Faker.imageUrl(640, 480);      // placeholder image URL
Faker.arrayElement(['a', 'b', 'c']);
Faker.oneOf('x', 'y', 'z');

String Utilities

import { Str } from '@nitronjs/framework';

Str.slug('Hello World');       // "hello-world"
Str.camel('user_name');        // "userName"
Str.pascal('user_name');       // "UserName"
Str.snake('userName');         // "user_name"
Str.kebab('userName');         // "user-name"
Str.title('hello world');      // "Hello World"
Str.ucfirst('hello');          // "Hello"
Str.random(32);                // random string
Str.uuid();                    // a UUID
Str.limit('Long text here', 9);// "Long text..."
Str.plural('post');            // "posts"
Str.singular('posts');         // "post"
Str.contains('hello', 'ell');  // true
Str.startsWith('hello', 'he'); // true

Global Functions

These functions are available everywhere — controllers, views, and client-side code — without an import:

| Function | Description | |---|---| | route(name, params?, query?) | Generate a URL for a named route. | | csrf() | The current request's CSRF token. Use it in forms and fetch headers. | | __(key, params?) | Translate a key with the current locale. | | lang(key, params?) | Alias of __(). | | request() | The current request object (Server Components only). Exposes path, method, query, params, headers, cookies, ip, isAjax, locale, session, auth. |

// In a form
<input type="hidden" name="_csrf" value={csrf()} />

// In a fetch call
fetch('/api/posts', {
    method: 'POST',
    headers: { 'x-csrf-token': csrf() }
});

// In a Server Component
const tab = request().query.tab || 'general';

CLI Commands

# Development
npm run dev              # Start dev server with HMR
npm run build            # Build for production
npm run start            # Start production server

# Database
npm run migrate          # Run migrations
npm run migrate:safe     # Run migrations after an automatic DB backup
npm run migrate:fresh    # Fresh migration
npm run migrate:fresh:seed # Fresh migration + seed
npm run migrate:rollback # Rollback last batch
npm run migrate:status   # Show migration status
npm run seed             # Run seeders
npx njs db:backup        # Manual DB backup for the current deploy target
npx njs db:backups       # List all DB recovery points
npx njs db:restore <id>  # Restore the DB from a backup (takes a pre-restore backup)

# Code Generation
npm run make:controller <name>
npm run make:model <name>
npm run make:middleware <name>
npm run make:migration <name>
npm run make:seeder <name>
npm run make:socket <name>

# Diagnostics
npx njs doctor           # Framework health check (auto-fixes safe drift)
npx njs deploy:doctor    # Deploy health check (cascades into doctor)

# Deployment
npx njs deploy:init      # Scaffold CI/CD files (workflow YAML, deploy.config.js)
npx njs deploy:runner    # Install a self-hosted GitHub Actions runner on a server
npx njs deploy:rollback  # Restore a previous snapshot interactively (on a server)

# Utilities
npm run storage:link     # Create storage symlink
npx njs key:generate     # Write a fresh APP_KEY into .env
npx njs key:generate --force  # Overwrite an existing APP_KEY (invalidates hashes/sessions/encrypted data)

Project Structure

my-app/
├── app/
│   ├── Controllers/      # Request handlers
│   ├── Middlewares/      # Custom middleware
│   ├── Models/           # Database models
│   ├── Sockets/          # Socket.IO handler classes (optional)
│   └── Kernel.js         # Middleware alias registry
├── config/               # Configuration files
│   ├── app.js
│   ├── auth.js
│   ├── database.js
│   ├── hash.js
│   ├── server.js
│   └── session.js
├── database/
│   ├── migrations/       # Database migrations
│   └── seeders/          # Database seeders
├── public/               # Static assets
├── resources/
│   ├── css/              # Stylesheets
│   ├── langs/            # Translation files
│   └── views/            # React components (TSX)
├── routes/
│   └── web.js            # Route definitions
├── storage/              # File storage (logs, sessions, uploads, snapshots)
├── deploy.config.js      # Deployment targets (created by deploy:init)
└── .env                  # Environment variables

CSS & Tailwind

Put your CSS files in resources/css/. Tailwind CSS v4 is automatically detected and processed.

/* resources/css/global.css */
@import "tailwindcss";

Import in your .tsx files using the @css alias:

import "@css/global.css";

Configuration

Accessing config values

import { Config } from '@nitronjs/framework';

const appLocale = Config.get('app.locale');
const sessionDriver = Config.get('session.driver', 'file');  // with a default

Config.get(key, default?) reads from the files in config/. The first key segment is the file name (appconfig/app.js), the rest is the path into that file's exported object.

Config files

| File | Configures | |---|---| | config/app.js | locale, fallback_locale, timezone, and the csp (Content Security Policy) whitelist. | | config/auth.js | defaults.guard and the guards map (provider model, login identifier, home / redirect routes). | | config/database.js | connections — per-driver settings (charset, collation, connection pool). | | config/session.js | driver, lifetime, cookieName, the cookie options, and csrf settings (which methods are protected, the token/header field names, route exceptions). | | config/server.js | web_server (body limit, multipart upload limits and security), cors, and log settings. | | config/hash.js | salt_rounds — the bcrypt cost factor. |

Environment variables

Configuration that varies per environment lives in .env:

APP_NAME=my-app
APP_KEY=
APP_URL=http://localhost
APP_PORT=3000

FILESYSTEM_DRIVER=disk

# Driver: mysql | postgresql | mongodb | none
DATABASE_DRIVER=none
DATABASE_HOST=127.0.0.1
DATABASE_PORT=3306
DATABASE_NAME=my-app
DATABASE_USERNAME=root
DATABASE_PASSWORD=

MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_SECURE=

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

APP_KEY is used for encryption and signing — generate a random value before going to production. Set DATABASE_DRIVER to none to run without a database.


Deployment

NitronJS ships a complete CI/CD pipeline: one config file, one GitHub Actions workflow, and snapshot-based rollback for every target you deploy to.

How it works

deploy.config.js at your project root lists every deployment target. The GitHub Actions workflow reads it, builds your app once, and deploys to each target in parallel via self-hosted runners. Every deploy takes a snapshot first, so a broken deploy rolls back automatically — and you can roll back manually any time with njs deploy:rollback.

Setup (once per project)

npx njs deploy:init      # creates .github/workflows/deploy.yml + deploy.config.js

Then edit deploy.config.js to add your targets:

/**
 * @type {import("@nitronjs/framework").DeployConfig}
 */
export default {
    defaults: {
        snapshot_keep: 5,
        backup_keep: 30
    },
    targets: {
        "my-app": {
            server: "prod-server",
            path: "/var/www/my-app"
        }
    }
};

Commit and push — the workflow is now active.

Setup (once per server)

On each server that hosts a target, install a self-hosted runner:

npx njs deploy:runner    # interactive: asks for label, repo URL, registration token

The server field in deploy.config.js must match the runner's label.

Deploying

Push to main (or trigger the workflow manually from the GitHub UI). The workflow builds once and deploys to every enabled target. Each deploy:

  1. Snapshots the current code (and DB, if a migration is pending)
  2. Extracts the new build
  3. Runs npm ci (skipped when package-lock.json is unchanged)
  4. Runs migrations with an automatic DB backup
  5. Reloads the app with PM2 and verifies it stays online

If any step fails, the snapshot is restored automatically.

Rolling back

On the target server, inside the deployment folder:

npx njs deploy:rollback  # interactive: pick a snapshot, confirm, done

A pre-rollback snapshot is taken first, so the rollback itself is reversible.

Health checks

npx njs doctor           # framework conventions — auto-fixes safe drift
npx njs deploy:doctor    # deploy readiness — run before pushing

Requirements

  • Node.js 20+
  • MySQL 5.7+ or MariaDB 10.3+

License

ISC