Clean. Elegant. Powerful.
A modern full-stack JavaScript framework combining React frontend with a robust Node.js backend, inspired by Laravel and Next.js philosophies but with a unique approach.
Documentation β’ Examples β’ Community
- Features
- Quick Start
- Architecture
- Project Structure
- Core Concepts
- Configuration
- CLI Commands
- Examples
- Supported Libraries
- Contributing
- Hybrid Routing - Object-based (simple) & File-based (automatic) routing systems
- Clean Code - Application code stays clean; framework complexity hidden in core
- Full-Stack Ready - React + Vite frontend with Node.js backend in one framework
- Modular Architecture - Built-in modules for Storage, Cache, and Logs with extensibility
- Database Agnostic - Easy switching between SQL (PostgreSQL, MySQL, SQLite) and NoSQL (MongoDB)
- Kernel Adapters - Flexible runtime adapters (Node.js, Bun, Deno ready)
- Type-Safe ORM - Drizzle ORM integration with zero-config setup
- Docker Ready - Production-ready Docker & Docker Compose configuration
- Code Quality - ESLint, Prettier, and modern tooling pre-configured
- Hot Reload - Development server with HMR for instant feedback
- Node.js 18+ (npm 9+)
- Docker & Docker Compose (optional, for containerization)
npm install -g @untrustnova/nova-cli
nova new my-nova-app
cd my-nova-app
npm install
npm run devDev URLs:
- Backend API:
http://localhost:3000 - Frontend (Vite):
http://localhost:5173
nova create:controller homenova db:pushnpm run build- Jika
nova devgagal karena Tailwind, install:npm install -D tailwindcss @tailwindcss/postcss autoprefixer postcss - Jika
novacommand tidak ditemukan, pastikannpm install -g @untrustnova/nova-clidan PATH sudah benar
Nova.js follows a layered architecture separating concerns into clear boundaries:
βββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer (User Land) β
β Controllers β’ Middleware β’ Routes β’ Models β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Framework Layer (Core) β
β Routing β’ Modules β’ Adapters β’ Kernel β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β Runtime Layer (Node.js/Bun/Deno) β
β HTTP β’ File System β’ Process Management β
βββββββββββββββββββββββββββββββββββββββββββββββ
Nova.js is built on three core principles:
- Clean Developer Experience (DX) - Your application code should be free from framework noise
- Zero-Config Setup - Sensible defaults work out of the box
- Production-Ready - Scales from prototypes to enterprise applications
Kernel Nova.js adalah pusat orkestrasi yang menjaga routing, modul, dan adapter runtime tetap bersih:
// node_modules/@untrustnova/nova-framework/core/kernel/index.js (ringkas)
import { createNodeAdapter } from './adapters/node.js';
const adapters = {
node: createNodeAdapter,
};
export class NovaKernel {
constructor(config) {
this.config = config;
this.adapter = adapters[config.kernel?.adapter || 'node']();
this.modules = new Map();
}
registerModule(name, factory) {
// defineModule akan mengikat lifecycle hook modul
}
async boot() {
// onInit -> load routes -> onReady
}
async start() {
await this.boot();
this.adapter.listen(this.config.server.port, this.config.server.host);
}
}Adapter adalah layer yang bisa diganti (Node, Bun, Deno) tanpa mengubah user-land code.
my-nova-app/
βββ app/ # π¦ User Land Application Code
β βββ controllers/
β β βββ home.controller.js
β β βββ api/
β β βββ posts.controller.js
β βββ middleware/
β β βββ auth.middleware.js
β β βββ cors.middleware.js
β βββ models/ # Database Models (Drizzle Schemas)
β β βββ user.model.js
β β βββ post.model.js
β βββ routes/
β β βββ web.js # Object-based routing
β β βββ api.js # API routes
β β βββ pages/ # File-based page routes
β β βββ index.js # GET /
β β βββ about.js # GET /about
β β βββ blog/
β β βββ index.js # GET /blog
β β βββ [slug].js # GET /blog/:slug
β βββ migrations/ # Database Migrations
β βββ 2024_01_create_users_table.js
β βββ 2024_02_create_posts_table.js
β
βββ web/ # π¨ Frontend (React + Vite)
β βββ components/
β β βββ Hero.jsx
β β βββ Navigation.jsx
β β βββ common/
β β βββ Button.jsx
β β βββ Card.jsx
β βββ pages/
β β βββ Dashboard.jsx
β β βββ NotFound.jsx
β βββ lib/
β β βββ api.js # API client helper
β β βββ hooks.js # Custom React hooks
β β βββ utils.js # Utility functions
β βββ styles/
β β βββ globals.css # Global Tailwind styles
β β βββ variables.css # CSS Variables
β βββ App.jsx
β βββ main.jsx
β βββ index.html
β
βββ public/ # πΌοΈ Static Assets
β βββ fonts/
β β βββ inter.woff2
β β βββ jetbrains-mono.woff2
β βββ images/
β β βββ logo.svg
β β βββ hero.webp
β β βββ icons/
β βββ favicon.ico
β
βββ storage/ # πΎ File Storage
β βββ app/
β β βββ uploads/
β β βββ exports/
β βββ cache/
β βββ logs/
β
βββ .env.local # Environment Variables
βββ .env.example # Environment Template
βββ .eslintrc.js # ESLint Configuration
βββ .prettierrc # Prettier Configuration
βββ jsconfig.json # Path Aliases
βββ nova.config.js # Framework Configuration
βββ vite.config.js # Vite (sync dari nova.config.js)
βββ server.js # Server Entry Point
βββ package.json
βββ docker-compose.yml
βββ Dockerfile
node_modules/
βββ @untrustnova/nova-framework/ # π§ Framework Core (dependency)
βββ package.json
βββ core/
βββ config/
βββ kernel/
β βββ adapters/
βββ routing/
βββ modules/
βββ db/
βββ middleware/
Clean, class-based controllers:
// app/controllers/home.controller.js
import { Controller } from '@untrustnova/nova-framework/controller';
export default class HomeController extends Controller {
async index({ response }) {
const posts = await this.db.query.posts.findMany();
response.json({ posts });
}
async show({ request, response }) {
const post = await this.db.query.posts.findFirst({
where: { id: request.params.id }
});
response.json({ post });
}
}// app/routes/web.js
import { route } from '@untrustnova/nova-framework/routing';
const routes = route();
routes.get('/', 'HomeController@index');
routes.get('/about', (request, response) => response.json({ page: 'about' }));
routes.post('/api/posts', 'PostsController@store');
export default routes.toArray();// app/routes/pages/index.js β GET /
export default async ({ response }) => {
response.json({ page: 'home' });
};
// app/routes/pages/about.js β GET /about
// app/routes/pages/home.index.js β GET /home
// app/routes/pages/blog/index.js β GET /blog
// app/routes/pages/blog/[slug].js β GET /blog/:slug// app/middleware/auth.middleware.js
export default class AuthMiddleware {
async handle(req, res, next) {
const token = req.headers.authorization?.split('Bearer ')[1];
if (!token) {
res.statusCode = 401;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Unauthorized' }));
}
try {
req.user = await verifyToken(token);
next();
} catch (err) {
res.statusCode = 401;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Invalid token' }));
}
}
}// app/models/user.model.js
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).unique().notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export default users;import { storage } from '@untrustnova/nova-framework/modules';
// Upload to local, S3, or Minio
const file = await storage.disk('s3').put('avatars/user.jpg', buffer);
const url = await storage.disk('s3').url('avatars/user.jpg');
await storage.disk('local').delete('temp/file.tmp');import { cache } from '@untrustnova/nova-framework/modules';
await cache.set('user:123', userData, 3600); // 1 hour TTL
const data = await cache.get('user:123');
await cache.forget('user:123');
await cache.flush();import { log } from '@untrustnova/nova-framework/modules';
log.info('User logged in', { userId: 123 });
log.error('Database error', error);
log.warning('Cache miss', { key: 'users' });
log.debug('Request info', { body: req.body });// app/bootstrap.js
import { app } from '@untrustnova/nova-framework';
app.onInit(async () => {
console.log('Initializing...');
});
app.onReady(async () => {
console.log('Ready to handle requests');
});
app.onShutdown(async () => {
console.log('Shutting down...');
});import 'dotenv/config';
import config from './nova.config.js';
import { NovaKernel } from '@untrustnova/nova-framework/kernel';
import { storageModule, cacheModule, logsModule } from '@untrustnova/nova-framework/modules';
const kernel = new NovaKernel(config);
kernel.registerModule('storage', storageModule);
kernel.registerModule('cache', cacheModule);
kernel.registerModule('logs', logsModule);
kernel.start();# App Settings
NOVA_APP_NAME=Nova App
NOVA_APP_URL=http://localhost:3000
NOVA_ENV=development
NOVA_DEBUG=true
# Server
NOVA_HOST=0.0.0.0
NOVA_PORT=3000
NOVA_KERNEL_ADAPTER=node
# Database (choose one)
NOVA_DB_CONNECTION=postgres
NOVA_DATABASE_URL=postgresql://user:password@localhost:5432/nova_db
# mysql://user:password@localhost:3306/nova_db
# file:./storage/db.sqlite
# mongodb://user:password@localhost:27017/nova_db
# supabase://project-ref.supabase.co?key=service-role-key
# Storage
NOVA_STORAGE_DRIVER=local
# NOVA_AWS_BUCKET=my-bucket
# NOVA_AWS_REGION=us-east-1
# NOVA_AWS_KEY=xxxxx
# NOVA_AWS_SECRET=xxxxx
# NOVA_MINIO_ENDPOINT=minio.example.com
# NOVA_MINIO_ACCESS_KEY=xxxxx
# NOVA_MINIO_SECRET_KEY=xxxxx
# Cache
NOVA_CACHE_DRIVER=memory
# NOVA_REDIS_URL=redis://localhost:6379
# Logging
NOVA_LOG_LEVEL=info
NOVA_LOG_CHANNEL=stack
# Frontend
VITE_API_URL=http://localhost:3000/apiimport { defineConfig, react, tailwindcss } from '@untrustnova/nova-framework/config';
export default defineConfig({
app: {
name: process.env.NOVA_APP_NAME || 'Nova',
url: process.env.NOVA_APP_URL || 'http://localhost:3000',
env: process.env.NOVA_ENV || 'development',
debug: process.env.NOVA_DEBUG === 'true',
},
server: {
host: process.env.NOVA_HOST || '0.0.0.0',
port: Number(process.env.NOVA_PORT || 3000),
},
security: {
bodyLimit: 1024 * 1024,
},
kernel: {
adapter: 'node', // 'node' | 'bun' | 'deno'
},
database: {
default: process.env.NOVA_DB_CONNECTION || 'postgres',
connections: {
postgres: {
driver: 'pg',
url: process.env.NOVA_DATABASE_URL,
},
mysql: {
driver: 'mysql2',
url: process.env.NOVA_MYSQL_URL,
},
sqlite: {
driver: 'better-sqlite3',
url: process.env.NOVA_SQLITE_URL,
},
mongodb: {
driver: 'mongodb',
url: process.env.NOVA_MONGODB_URL,
},
},
},
modules: {
storage: {
driver: process.env.NOVA_STORAGE_DRIVER || 'local',
disks: {
local: { root: './storage/app' },
s3: {
key: process.env.NOVA_AWS_KEY,
secret: process.env.NOVA_AWS_SECRET,
bucket: process.env.NOVA_AWS_BUCKET,
region: process.env.NOVA_AWS_REGION,
},
minio: {
endPoint: process.env.NOVA_MINIO_ENDPOINT,
port: 9000,
useSSL: false,
accessKey: process.env.NOVA_MINIO_ACCESS_KEY,
secretKey: process.env.NOVA_MINIO_SECRET_KEY,
},
},
},
cache: {
driver: process.env.NOVA_CACHE_DRIVER || 'memory',
redis: {
url: process.env.NOVA_REDIS_URL,
},
},
logs: {
level: process.env.NOVA_LOG_LEVEL || 'info',
channels: {
stack: ['single', 'slack'],
single: { driver: 'single', path: './storage/logs/nova.log' },
},
},
},
frontend: {
...react(),
...tailwindcss({
content: ['./web/**/*.{jsx,js}'],
}),
},
alias: {
'@': './web',
'@components': './web/components',
'@lib': './web/lib',
},
});npm install -g nova-cli
nova new my-app # Create new project
npm run dev # Start dev server with HMR
npm run build # Build for production
npm start # Run production servernova create:controller posts # New controller
nova create:middleware auth # New middleware
nova create:model user # New model
nova create:migration create_users_tablenova db:init # Initialize database
nova db:push # Run pending migrations
nova db:rollback # Undo last migration
nova db:reset # Drop and recreate
nova db:seed # Seed with test datanpm run lint # Check code quality
npm run format # Auto-format code
npm test # Run tests
npm run type-check # TypeScript check// app/models/post.model.js
import { pgTable, serial, varchar, text, timestamp } from 'drizzle-orm/pg-core';
import { users } from './user.model.js';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
slug: varchar('slug', { length: 255 }).unique().notNull(),
content: text('content').notNull(),
excerpt: varchar('excerpt', { length: 500 }),
authorId: serial('author_id').references(() => users.id),
featured: varchar('featured', { length: 255 }),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});// app/controllers/posts.controller.js
import { Controller } from '@untrustnova/nova-framework/controller';
import { storage, cache } from '@untrustnova/nova-framework/modules';
import { posts } from '../models/post.model.js';
export default class PostsController extends Controller {
async index({ response }) {
const cached = await cache.get('posts:list');
if (cached) return response.json({ posts: cached });
const allPosts = await this.db.query.posts.findMany();
await cache.set('posts:list', allPosts, 3600);
response.json({ posts: allPosts });
}
async store({ request, response }) {
const { title, content, excerpt } = request.body;
const slug = title.toLowerCase().replace(/\s+/g, '-');
let featuredPath = null;
if (request.file) {
featuredPath = await storage
.disk('s3')
.put(`posts/${slug}/${request.file.originalname}`, request.file.buffer);
}
const post = await this.db.insert(posts).values({
title,
slug,
content,
excerpt,
featured: featuredPath,
});
await cache.forget('posts:list');
response.status(201).json({ post });
}
async show({ request, response }) {
const post = await this.db.query.posts.findFirst({
where: { slug: request.params.slug },
});
if (!post) return response.status(404).json({ error: 'Not found' });
response.json({ post });
}
async destroy({ request, response }) {
const post = await this.db.query.posts.findFirst({
where: { id: request.params.id },
});
if (post?.featured) {
await storage.disk('s3').delete(post.featured);
}
await this.db.delete(posts).where({ id: req.params.id });
await cache.forget('posts:list');
response.json({ success: true });
}
}// app/routes/api.js
import { route } from '@untrustnova/nova-framework/routing';
import multer from 'multer';
const upload = multer({ storage: multer.memoryStorage() });
const routes = route();
routes.get('/api/posts', 'PostsController@index');
routes.get('/api/posts/:slug', 'PostsController@show');
routes.post('/api/posts', upload.single('featured'), 'PostsController@store');
routes.delete('/api/posts/:id', 'PostsController@destroy');
export default routes.toArray();// web/components/BlogList.jsx
import { useEffect, useState } from 'react';
import { api } from '@lib/api';
export default function BlogList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.get('/posts')
.then(({ posts }) => {
setPosts(posts);
setLoading(false);
})
.catch(err => console.error(err));
}, []);
if (loading) return <div className="p-4">Loading...</div>;
return (
<div className="grid gap-4 p-6">
{posts.map((post) => (
<article
key={post.id}
className="border rounded-lg p-4 hover:shadow-lg transition"
>
<h2 className="text-xl font-bold">{post.title}</h2>
<p className="text-gray-600 mt-2">{post.excerpt}</p>
{post.featured && (
<img
src={post.featured}
alt={post.title}
className="mt-4 rounded max-w-full"
/>
)}
<a href={`/blog/${post.slug}`} className="text-blue-500 mt-4 block">
Read More β
</a>
</article>
))}
</div>
);
}# Start all services
docker-compose up -d
# View logs
docker-compose logs -f app
# Stop services
docker-compose downIncludes: Node.js app, PostgreSQL, Redis, persistent volumes
- PostgreSQL -
pg - MySQL -
mysql2 - SQLite -
better-sqlite3 - MongoDB -
mongodb - Supabase -
supabase
- Drizzle ORM (Built-in)
- React 19+
- Vite 6+
- Tailwind CSS Latest
- Memory (default)
- Redis
- Local Filesystem
- AWS S3
- MinIO
- ESLint (Code quality)
- Prettier (Formatting)
- Nodemon (Auto-restart)
- Vite HMR (Hot reload)
- Express.js / Hono
- Multer (File uploads)
- Dotenv (Environment vars)
- CORS, Compression
| Layer | Tech | Version |
|---|---|---|
| Frontend | React, Vite, Tailwind | 19+, 6+, Latest |
| Backend | Node.js | 18+, Latest |
| Database | PostgreSQL, MySQL, SQLite, MongoDB | Latest |
| ORM | Drizzle | 0.40+ |
| Cache | Redis, Memory | Latest |
| Storage | S3, MinIO, Local | Latest |
| Build | Vite | 6.0+ |
| Container | Docker, Docker Compose | Latest |
git clone https://github.com/nova-js/nova.git
git checkout -b feature/amazing-feature
git commit -am 'Add feature'
git push origin feature/amazing-featureRead CONTRIBUTING.md for guidelines.
MIT License - See LICENSE
- Docs: docs.nova-js.dev
- Discord: Join Community
- Issues: GitHub Issues
- Discussions: Discussions
Made with β€οΈ by the Nova.js Community