NeoPG is a high-performance, zero-dependency ORM built directly on top of postgres.js — the fastest PostgreSQL client for Node.js.
It bridges the gap between the developer experience (DX) of a chainable Query Builder and the raw performance of native SQL Template Literals.
- Powered by postgres.js: Inherits the incredible speed and stability of the fastest PG client.
- Zero Dependencies: The core driver is vendored and optimized internally. No heavy dependency tree.
- Hybrid API: Enjoy the ease of Chainable/Fluent APIs (like
.where().select()) combined with the power of Tagged Template Literals. - Performance First: Zero string concatenation logic. All queries are compiled into efficient fragments and executed natively.
- Auto Schema Sync: define your models in code, and NeoPG syncs the table structure, indices, and foreign keys automatically.
- Type Smart: Automatic type casting for aggregations (
sum,avgreturns numbers, not strings) and JSON handling.
npm install neopgconst NeoPG = require('neopg'); const config = { host: 'localhost', port: 5432, database: 'my_db', user: 'postgres', password: 'password', max: 10, // Connection pool size idle_timeout: 30, // Idle connection timeout in seconds debug: false, // Enable query logging schema: 'public' // Default schema }; const db = new NeoPG(config);await db.close()Create a model file (e.g., models/User.js). Your class should extend NeoPG.ModelChain.
const { ModelChain, dataTypes } = require('neopg') class User extends ModelChain { static schema = { tableName: 'users', modelName: 'User', // Optional, defaults to tableName primaryKey: 'id', // Auto-sync table structure based on this definition column: { id: { type: dataTypes.ID, // Auto-generates Snowflake-like ID }, username: { type: dataTypes.STRING(100), required: true }, email: { type: dataTypes.STRING(255), required: true }, age: { type: dataTypes.INT, default: 18 }, meta: { type: dataTypes.JSONB }, created_at: { type: dataTypes.BIGINT, timestamp: 'insert' // Auto-fill on insert }, updated_at: { type: dataTypes.BIGINT, timestamp: 'update' // Auto-fill on insert & update } }, // Indexes index: ['email', 'age'], unique: ['username'] } } module.exports = UserNeoPG includes a built-in CLI tool to quickly generate model files with boilerplate code.
Run via npx (no global installation required):
npx neopg-model [options] [model_names...]--dir=<path>: Specify the output directory (default:./model).
1. Basic Generation
npx neopg-model user # Creates: ./model/user.js # Class: User # Table: user2. Naming Convention (Hyphenated) NeoPG automatically converts hyphenated names to CamelCase for the class and snake_case for the table.
npx neopg-model user-log # Creates: ./model/user-log.js # Class: UserLog # Table: user_log3. Multiple Models & Custom Directory
npx neopg-model --dir=./src/models product order-item # Creates: # ./src/models/product.js # ./src/models/order-item.js4. ES Modules (.mjs) If you suffix the name with .mjs, it generates ESM syntax (export default).
npx neopg-model config.mjsInitialize NeoPG and register your models. You can define models using classes or configuration objects.
NeoPG provides three methods for registration:
define(model): The standard method. Throws an error if a model with the same name already exists.add(model): Alias fordefine.set(model): Overwrites the existing model if the name conflicts. Useful for hot-reloading or dynamic schema updates.
const User = require('./models/User') // 1. Standard Registration (Safe) // Will throw error: "[NeoPG] modelName conflict: User" if registered twice db.define(User) // 2. Force Overwrite (Reset) // Updates the definition for 'User' even if it exists db.set(User) // 3. Register using a plain object (Quick prototype) db.define({ tableName: 'logs', column: { message: 'string', level: 'int' } }) console.log(db.has('User'))Sync the table structure to the database based on registered models.
// Sync Table Structure (DDL) // options: { force: true } will drop columns not defined in schema await db.sync({ force: false }) console.log('Database synced!')Instead of manually importing and defining each model, you can load all models from a directory.
Rules:
- Only
.jsand.mjsfiles are loaded. - Files starting with
_are ignored (useful for utils/helpers). - Files starting with
!are ignored (useful for disabled models).
const db = new NeoPG(config) // Load all models from the './models' directory // This is asynchronous because it supports .mjs dynamic imports await db.loadModels('./models') //load esm modules await db.loadModels('./esmodels', 'esm') //load files await db.loadFiles(['./models2/WxUser.js', './models2/Role.js']) // Now you can sync and use them await db.sync()NeoPG provides a fluent, chainable API that feels natural to use.
// Get all users const users = await db.model('User').find(); // Select specific columns const users = await db.model('User') .select('id, username') .limit(10) .find(); // Get a single record const user = await db.model('User').where({ id: '123' }).get(); // Pagination const page2 = await db.model('User') .select(['id', 'username', 'role']) .page(2, 20) .find(); // Page 2, Size 20await db.model('User') .where({ age: 18, status: 'active' }) .where('create_time', '>', 1600000000) .where('id IS NOT NULL') .select(db.sql`id, username, role`) .find()This is where NeoPG shines. You can mix raw SQL fragments safely using the sql tag from the context.
// db.sql is the native postgres instance const { sql } = db; await db.model('User') .where({ status: 'active' }) // Safe parameter injection via Template Literals .where(sql`age > ${20} AND email LIKE ${'%@gmail.com'}`) .find();NeoPG handles type casting automatically (e.g., converting PostgreSQL count string results to Javascript Numbers).
// Count const total = await db.model('User').where({ age: 18 }).count(); // Max / Min const maxAge = await db.model('User').max('age'); // Sum / Avg (Returns Number, not String) const totalScore = await db.model('User').sum('score'); const avgScore = await db.model('User').avg('score'); // Group By const stats = await db.model('User') .select('city, count(*) as num') .group('city') .find();// Insert one const newUser = await db.model('User').insert({ username: 'neo', email: 'neo@matrix.com' }) // ID and Timestamps are automatically generated if configured in Schema // Insert multiple (Batch) await db.model('User').insert([ { username: 'a' }, { username: 'b' } ])const updated = await db.model('User') .where({ id: '123' }) .update({ age: 99, meta: { role: 'admin' } });await db.model('User') .where('age', '<', 10) .delete();By default, write operations might not return data depending on the driver optimization. You can enforce it:
const deletedUsers = await db.model('User') .where('status', 'banned') .returning('id, username') // or returning('*') .delete();NeoPG exposes the full power of postgres.js. You don't need the ModelChain for everything.
📚 Reference: Full documentation for the SQL tag can be found at the postgres.js GitHub page.
// Access the native driver const sql = db.sql; // Execute raw SQL safely const users = await sql` SELECT * FROM users WHERE age > ${20} `; // Dynamic tables/columns using helper const table = 'users'; const column = 'age'; const result = await sql` SELECT ${sql(column)} FROM ${sql(table)} `;NeoPG provides a unified transaction API. It supports nested transactions (Savepoints) automatically.
// Start a transaction scope const result = await db.transaction(async (tx) => { // 'tx' is a TransactionScope that mimics 'db' // 1. Write operation const user = await tx.model('User').insert({ username: 'alice' }); // 2. Read operation within transaction const count = await tx.model('User').count() // 3. Throwing an error will automatically ROLLBACK if (count > 100) { throw new Error('Limit reached') } return user })await db.sql.begin(async (sql) => { // sql is the transaction connection await sql`INSERT INTO users (name) VALUES ('bob')`; })ISC

