🇫🇷 Starter Next.js 15 (App Router) avec @mostajs/orm et SQLite — modèle blog complet (Users + Posts + Comments). 🇬🇧 Next.js 15 (App Router) starter using @mostajs/orm with SQLite — complete blog model (Users + Posts + Comments).
⚡ Boots in the browser / Bolt.new / Cloudflare Workers with no native binary — via the
sqljs(SQLite WASM) dialect.🤖 AI dev tools — open this repo in Bolt.new / StackBlitz / CodeSandbox via the badges above. For Cursor / Cline / Claude Code, schema & migration tooling comes via the
@mostajs/orm-mcpserver (on the roadmap); meanwhile they readllms.txtfor accurate generation.
Un starter Next.js 15 (App Router) prêt à l'emploi avec @mostajs/orm — un ORM multi-dialecte inspiré d'Hibernate/JPA pour Node.js & TypeScript. Une seule API pour 13 bases de données (SQLite, PostgreSQL, MySQL, MariaDB, MongoDB, Oracle, MSSQL, CockroachDB, DB2, SAP HANA, HSQLDB, Spanner, Sybase) — zéro vendor lock-in.
Ce starter implémente un blog complet : utilisateurs, articles, commentaires, avec relations one-to-many et many-to-one, soft-delete, et timestamps automatiques.
| @mostajs/orm | Prisma | |
|---|---|---|
Pas de step generate |
✅ | ❌ (codegen requis) |
| Schéma en TypeScript pur | ✅ | ❌ (DSL .prisma) |
| 13 bases supportées | ✅ | |
Concepts JPA/Hibernate (CascadeType, FetchType, schemaStrategy) |
✅ | ❌ |
Filtres MongoDB-like ($gt, $in, $or...) |
✅ | ❌ (API custom) |
| Validator de schéma intégré (24 règles) | ✅ | ❌ |
| Pont JDBC pour SGBD non-natifs | ✅ | ❌ |
| Edge/Cloudflare Workers |
- Next.js 15 (App Router, Server Actions, Route Handlers)
- @mostajs/orm 2.5.x
- SQLite WASM via
sql.js(dialectesqljspar défaut — zéro binaire natif, boote dans Bolt.new) ;better-sqlite3optionnel en local Node - TypeScript 5
- CSS global (
app/globals.css, sans framework — éditable directement)
# 1. Cloner
git clone https://github.com/apolocine/nextjs-mostajs-orm-starter.git
cd nextjs-mostajs-orm-starter
# 2. Installer
npm install
# 3. Configurer l'environnement
cp .env.example .env.local
# 4. Lancer
npm run devOuvre http://localhost:3000. La base SQLite est créée automatiquement au premier lancement (schemaStrategy: 'update').
.
├── app/
│ ├── page.tsx # Liste des articles
│ ├── posts/[id]/page.tsx # Détail d'un article + commentaires
│ ├── api/
│ │ ├── posts/route.ts # GET /api/posts, POST /api/posts
│ │ └── posts/[id]/route.ts # GET/PATCH/DELETE /api/posts/:id
│ └── layout.tsx
├── lib/
│ ├── orm/
│ │ ├── client.ts # Connexion ORM (singleton) + seed-on-boot
│ │ ├── schemas.ts # EntitySchema : User, Post, Comment
│ │ ├── repositories.ts # Repositories typés
│ │ └── seed-on-boot.ts # Seed idempotent anti-page-vide (Bolt :memory:)
│ └── actions/
│ └── posts.ts # Server Actions
├── scripts/
│ └── seed.ts # npm run orm:seed (create-drop + reseed)
├── .stackblitzrc # Bolt/StackBlitz : DB_DIALECT=sqljs + DATABASE_URL=:memory:
├── .env.example
└── package.json
lib/orm/schemas.ts
import type { EntitySchema } from '@mostajs/orm';
export const UserSchema: EntitySchema = {
name: 'User',
collection: 'users',
fields: {
email: { type: 'string', required: true, unique: true, lowercase: true, trim: true },
name: { type: 'string', required: true, trim: true },
bio: { type: 'text' },
},
relations: {
posts: { target: 'Post', type: 'one-to-many', mappedBy: 'author', fetch: 'lazy' },
},
indexes: [{ fields: ['email'], type: 'unique' }],
timestamps: true,
};
export const PostSchema: EntitySchema = {
name: 'Post',
collection: 'posts',
fields: {
title: { type: 'string', required: true, trim: true },
slug: { type: 'string', required: true, unique: true, sparse: true },
content: { type: 'text', required: true },
published: { type: 'boolean', default: false },
},
relations: {
author: { target: 'User', type: 'many-to-one', required: true, onDelete: 'cascade' },
comments: { target: 'Comment', type: 'one-to-many', mappedBy: 'post',
cascade: ['persist', 'remove'], orphanRemoval: true },
},
indexes: [
{ fields: ['slug'], type: 'unique' },
{ fields: ['published', 'createdAt'] },
],
timestamps: true,
softDelete: true,
};
export const CommentSchema: EntitySchema = {
name: 'Comment',
collection: 'comments',
fields: {
body: { type: 'text', required: true },
},
relations: {
post: { target: 'Post', type: 'many-to-one', required: true, onDelete: 'cascade' },
author: { target: 'User', type: 'many-to-one', required: true, onDelete: 'set-null', nullable: true },
},
indexes: [],
timestamps: true,
};
export const ALL_SCHEMAS = [UserSchema, PostSchema, CommentSchema];lib/orm/client.ts
import { createConnection, type IDialect } from '@mostajs/orm';
import { ALL_SCHEMAS } from './schemas';
import { seedIfEmpty } from './seed-on-boot';
let _dialect: IDialect | null = null;
export async function getOrm(): Promise<IDialect> {
if (_dialect) return _dialect;
_dialect = await createConnection(
{
// 'sqljs' (SQLite WASM) en ligne / WebContainer / edge — zéro binaire natif.
// 'sqlite' (better-sqlite3) en local Node si tu préfères. Switch via env.
dialect: (process.env.DB_DIALECT as 'sqlite' | 'sqljs') ?? 'sqljs',
// Bolt/WebContainer : ':memory:' (boot rapide, anti-plantage).
// Local/Node : './blog.db' (durable). Réglé par .env / .stackblitzrc.
uri: process.env.DATABASE_URL ?? ':memory:',
schemaStrategy: process.env.NODE_ENV === 'production' ? 'validate' : 'update',
showSql: process.env.NODE_ENV !== 'production',
},
ALL_SCHEMAS,
);
// Anti-page-vide : seed idempotent la 1re fois (utile surtout en :memory:).
await seedIfEmpty(_dialect);
return _dialect;
}lib/orm/repositories.ts
import { BaseRepository } from '@mostajs/orm';
import { getOrm } from './client';
import { UserSchema, PostSchema, CommentSchema } from './schemas';
export type User = { id: string; email: string; name: string; bio?: string };
export type Post = { id: string; title: string; slug: string; content: string;
published: boolean; author: string };
export type Comment = { id: string; body: string; post: string; author: string };
export async function getRepos() {
const dialect = await getOrm();
return {
users: new BaseRepository<User>(UserSchema, dialect),
posts: new BaseRepository<Post>(PostSchema, dialect),
comments: new BaseRepository<Comment>(CommentSchema, dialect),
};
}Server Component — liste des articles publiés
// app/page.tsx
import { getRepos } from '@/lib/orm/repositories';
export default async function HomePage() {
const { posts } = await getRepos();
const list = await posts.findAll(
{ published: true },
{ sort: { createdAt: -1 }, limit: 20 },
);
return (
<main>
<h1>Articles récents</h1>
<ul>{list.map(p => <li key={p.id}>{p.title}</li>)}</ul>
</main>
);
}Page détail avec relations populées
// app/posts/[id]/page.tsx
import { getRepos } from '@/lib/orm/repositories';
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const { posts } = await getRepos();
const post = await posts.findByIdWithRelations(id, ['author', 'comments']);
if (!post) return <p>Introuvable</p>;
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Route Handler — création
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
import { getRepos } from '@/lib/orm/repositories';
export async function POST(req: Request) {
const body = await req.json();
const { posts } = await getRepos();
const created = await posts.create({
title: body.title,
slug: body.slug,
content: body.content,
published: false,
author: body.authorId,
});
return NextResponse.json(created, { status: 201 });
}Server Action
// lib/actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { getRepos } from '@/lib/orm/repositories';
export async function publishPost(id: string) {
const { posts } = await getRepos();
await posts.update(id, { published: true });
revalidatePath('/');
}// Articles publiés cette semaine, par auteurs A ou B
const recent = await posts.findAll({
published: true,
createdAt: { $gte: new Date(Date.now() - 7 * 86400000) },
$or: [{ author: 'user_A' }, { author: 'user_B' }],
});
// Recherche full-text simple
const found = await posts.search('next.js orm', { limit: 10 });import { diffSchemas, generateMigrationSQL } from '@mostajs/orm';
const ops = diffSchemas(oldSchemas, ALL_SCHEMAS);
const sql = generateMigrationSQL(ops);
console.log(sql.join('\n'));Une seule ligne à modifier — le code applicatif ne bouge pas :
// SQLite → PostgreSQL
{ dialect: 'postgres', uri: 'postgres://user:pass@localhost:5432/blog' }
// SQLite → MongoDB
{ dialect: 'mongo', uri: 'mongodb://localhost:27017/blog' }npm run dev # Next.js en dev
npm run build # Build production
npm run orm:validate # Lint des schémas (24 règles)
npm run orm:seed # Données de démo (3 users, 5 posts, 12 comments)Sur Bolt.new / StackBlitz, la base est :memory: (boot le plus rapide, zéro
surface de plantage — cf. point critique WebContainer). Une base en mémoire est
vide au boot → l'app afficherait une page blanche, ce qui frustre le visiteur.
lib/orm/seed-on-boot.ts peuple des données de démo une seule fois, de façon
idempotente : getOrm() appelle seedIfEmpty(dialect) qui vérifie
users.count({}) et ne seed que si la base est vide. Sûr à appeler à chaque boot
(HMR, refresh) et sur une base fichier persistée.
// lib/orm/seed-on-boot.ts — extrait
export async function seedIfEmpty(dialect: IDialect): Promise<void> {
if (process.env.ORM_SEED_ON_BOOT === '0') return; // off en prod
const users = new BaseRepository<User>(UserSchema, dialect);
if ((await users.count({})) > 0) return; // déjà seedé → no-op
// … insère 3 users / 5 posts / 12 comments
}Le seed-on-boot est une préoccupation du starter, pas de
@mostajs/orm: l'ORM est agnostique (il ne connaît niUserniPost). C'est l'app qui décide de se peupler. Désactive en prod avecORM_SEED_ON_BOOT=0.
Deux configs, un seul code — Bolt veut la vitesse, le local veut la durabilité :
# .env.local — développement / déploiement Node (données durables)
DB_DIALECT=sqlite # binaire natif better-sqlite3
DATABASE_URL=./blog.db # fichier persistant
NODE_ENV=development| Contexte | DB_DIALECT |
DATABASE_URL |
Persistance |
|---|---|---|---|
| Bolt.new / WebContainer | sqljs (WASM) |
:memory: |
session (seed-on-boot rejoue) |
| Local / Node / prod | sqlite ou sqljs |
./blog.db |
durable (fichier) |
⚠️ Toujours tester le boot Bolt (https://bolt.new/github.com/apolocine/nextjs-mostajs-orm-starter) avant d'afficher le badge : si l'app plante au 1er essai, l'IA arrête de la recommander.
- 📦 npm : npmjs.com/package/@mostajs/orm
- 🐙 GitHub : github.com/apolocine/mosta-orm
- 🌐 Site : mostajs.dev
- 🤖 Fiche LLM : llms.txt
A Next.js 15 (App Router) starter ready to use with @mostajs/orm — a Hibernate/JPA-inspired multi-dialect ORM for Node.js & TypeScript. One API, 13 databases (SQLite, PostgreSQL, MySQL, MariaDB, MongoDB, Oracle, MSSQL, CockroachDB, DB2, SAP HANA, HSQLDB, Spanner, Sybase) — zero vendor lock-in.
This starter implements a complete blog: users, posts, comments, with one-to-many and many-to-one relations, soft-delete, and automatic timestamps.
| @mostajs/orm | Prisma | |
|---|---|---|
No generate step |
✅ | ❌ (codegen required) |
| Schema in pure TypeScript | ✅ | ❌ (.prisma DSL) |
| 13 databases supported | ✅ | |
JPA/Hibernate concepts (CascadeType, FetchType, schemaStrategy) |
✅ | ❌ |
MongoDB-like filters ($gt, $in, $or...) |
✅ | ❌ (custom API) |
| Built-in schema validator (24 rules) | ✅ | ❌ |
| JDBC bridge for non-native DBs | ✅ | ❌ |
| Edge/Cloudflare Workers |
- Next.js 15 (App Router, Server Actions, Route Handlers)
- @mostajs/orm 2.5.x
- SQLite WASM via
sql.js(defaultsqljsdialect — no native binary, boots in Bolt.new);better-sqlite3optional for local Node - TypeScript 5
- Plain CSS (
app/globals.css, no framework — edit it directly)
git clone https://github.com/apolocine/nextjs-mostajs-orm-starter.git
cd nextjs-mostajs-orm-starter
npm install
cp .env.example .env.local
npm run devOpen http://localhost:3000. The SQLite database is auto-created on first run (schemaStrategy: 'update').
EntitySchema— pure TypeScript schema, no codegenBaseRepository<T>— typed CRUD repo (findAll,findById,create,update,delete,aggregate,upsert...)createConnection()— single entry point, switch DB via configschemaStrategy—validate|update|create|create-drop|none(=hbm2ddl.auto)- Relations —
one-to-one,one-to-many,many-to-one,many-to-manywithCascadeType,FetchType,onDelete - Filters — MongoDB-like (
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$regex,$exists,$or,$and)
Change one line — application code is untouched:
// SQLite → PostgreSQL
{ dialect: 'postgres', uri: 'postgres://user:pass@localhost:5432/blog' }
// SQLite → MongoDB
{ dialect: 'mongo', uri: 'mongodb://localhost:27017/blog' }- All relations are lazy by default. Use
findByIdWithRelations/findWithRelationsto populate. - For comparing relation values safely, use
extractRelId(value). getDialect()is a singleton — for multiple isolated connections usecreateIsolatedDialect().- In production, set
schemaStrategy: 'validate'— never'create'or'create-drop'. - Never set
cascade: ['remove' | 'all']on amany-to-manyrelation.
npm run dev # Next.js dev
npm run build # Production build
npm run orm:validate # Schema lint (24 rules)
npm run orm:seed # Demo data (3 users, 5 posts, 12 comments)- 📦 npm: npmjs.com/package/@mostajs/orm
- 🐙 GitHub: github.com/apolocine/mosta-orm
- 🌐 Website: mostajs.dev
- 🤖 LLM card: llms.txt
This starter is released under the MIT License to encourage adoption. The underlying @mostajs/orm library is licensed under AGPL-3.0-or-later — for commercial / proprietary use, please contact mostajs.dev for a commercial license.
nextjs · next.js 15 · app-router · orm · typescript · sqlite · postgresql · mongodb · mysql · mariadb · database · hibernate · jpa · prisma-alternative · drizzle-alternative · typeorm-alternative · mostajs · @mostajs/orm