From 6a5dd85df2ba01f58828e6f7398c999a0e061e6d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 2 Aug 2026 14:30:51 -0500 Subject: [PATCH 1/6] docs: rewrite database integration guide around modern drivers and best practices Replace the outdated per-database driver catalog (deprecated packages, callback-era APIs) with a single modern example using node:sqlite in cjs, mjs, and ts variants, links to the official docs of other popular drivers, an overview of ORMs and query builders, and a best practices section covering connection pooling, parameterized queries, credentials, error handling, request cancellation with req.signal, and graceful shutdown. --- .../pages/en/guide/database-integration.mdx | 678 +++--------------- 1 file changed, 96 insertions(+), 582 deletions(-) diff --git a/src/content/pages/en/guide/database-integration.mdx b/src/content/pages/en/guide/database-integration.mdx index 5c3aeab058..996d1ee80a 100644 --- a/src/content/pages/en/guide/database-integration.mdx +++ b/src/content/pages/en/guide/database-integration.mdx @@ -1,656 +1,170 @@ --- title: Database integration -description: Discover how to integrate various databases with Express.js applications, including setup examples for MongoDB, MySQL, PostgreSQL, and more. +description: Learn how to connect databases to Express.js applications, when to use an ORM, and best practices for connection pooling, security, and error handling. --- -import Alert from '@components/primitives/Alert/Alert.astro'; -import PackageManagerCommand from '@components/patterns/PackageManagerCommand/PackageManagerCommand.astro'; +Express doesn't include database support out of the box, and it doesn't need to. Connecting a database to an Express app is just a matter of loading an appropriate Node.js driver or ORM in your app. This page shows the general pattern using SQLite as an example, points you to the official documentation for other popular databases, and covers a few practices that apply no matter which database you choose. -Adding the capability to connect databases to Express apps is just a matter of loading an appropriate Node.js driver for the database in your app. This document briefly explains how to add and use some of the most popular Node.js modules for database systems in your Express app: - -- [Cassandra](#cassandra) -- [Couchbase](#couchbase) -- [CouchDB](#couchdb) -- [LevelDB](#leveldb) -- [MySQL](#mysql) -- [MongoDB](#mongodb) -- [Neo4j](#neo4j) -- [Oracle](#oracle) -- [PostgreSQL](#postgresql) -- [Redis](#redis) -- [SQL Server](#sql-server) -- [SQLite](#sqlite) -- [Elasticsearch](#elasticsearch) - - - -These database drivers are among many that are available. For other options, search on the -[npm](https://www.npmjs.com/) site. - - - -## Cassandra - -**Module**: [cassandra-driver](https://github.com/datastax/nodejs-driver) - -### Installation - - - -### Example - -```cjs title="index.cjs" -const cassandra = require('cassandra-driver'); -const client = new cassandra.Client({ contactPoints: ['localhost'] }); - -client.execute('select key from system.local', (err, result) => { - if (err) throw err; - console.log(result.rows[0]); -}); -``` - -```mjs title="index.mjs" -import cassandra from 'cassandra-driver'; - -const client = new cassandra.Client({ contactPoints: ['localhost'] }); - -client.execute('select key from system.local', (err, result) => { - if (err) throw err; - console.log(result.rows[0]); -}); -``` - -## Couchbase - -**Module**: [couchnode](https://github.com/couchbase/couchnode) - -### Installation - - - -### Example - -```cjs title="index.cjs" -const couchbase = require('couchbase'); -const bucket = new couchbase.Cluster('http://localhost:8091').openBucket('bucketName'); - -// add a document to a bucket -bucket.insert('document-key', { name: 'Matt', shoeSize: 13 }, (err, result) => { - if (err) { - console.log(err); - } else { - console.log(result); - } -}); - -// get all documents with shoe size 13 -const n1ql = 'SELECT d.* FROM `bucketName` d WHERE shoeSize = $1'; -const query = N1qlQuery.fromString(n1ql); -bucket.query(query, [13], (err, result) => { - if (err) { - console.log(err); - } else { - console.log(result); - } -}); -``` - -```mjs title="index.mjs" -import couchbase from 'couchbase'; - -const bucket = new couchbase.Cluster('http://localhost:8091').openBucket('bucketName'); - -// add a document to a bucket -bucket.insert('document-key', { name: 'Matt', shoeSize: 13 }, (err, result) => { - if (err) { - console.log(err); - } else { - console.log(result); - } -}); - -// get all documents with shoe size 13 -const n1ql = 'SELECT d.* FROM `bucketName` d WHERE shoeSize = $1'; -const query = N1qlQuery.fromString(n1ql); -bucket.query(query, [13], (err, result) => { - if (err) { - console.log(err); - } else { - console.log(result); - } -}); -``` - -## CouchDB - -**Module**: [nano](https://github.com/dscape/nano) - -### Installation - - - -### Example - -```js title="index.js" -const nano = require('nano')('http://localhost:5984'); -nano.db.create('books'); -const books = nano.db.use('books'); - -// Insert a book document in the books database -books.insert({ name: 'The Art of war' }, null, (err, body) => { - if (err) { - console.log(err); - } else { - console.log(body); - } -}); - -// Get a list of all books -books.list((err, body) => { - if (err) { - console.log(err); - } else { - console.log(body.rows); - } -}); -``` - -## LevelDB - -**Module**: [levelup](https://github.com/rvagg/node-levelup) - -### Installation - - - -### Example - -```cjs title="index.cjs" -const levelup = require('levelup'); -const db = levelup('./mydb'); - -db.put('name', 'LevelUP', (err) => { - if (err) return console.log('Ooops!', err); - - db.get('name', (err, value) => { - if (err) return console.log('Ooops!', err); - - console.log(`name=${value}`); - }); -}); -``` - -```mjs title="index.mjs" -import levelup from 'levelup'; - -const db = levelup('./mydb'); - -db.put('name', 'LevelUP', (err) => { - if (err) return console.log('Ooops!', err); - - db.get('name', (err, value) => { - if (err) return console.log('Ooops!', err); - - console.log(`name=${value}`); - }); -}); -``` - -## MySQL - -**Module**: [mysql](https://github.com/felixge/node-mysql/) - -### Installation +## SQLite - +Node.js includes a built-in SQLite client, [node:sqlite](https://nodejs.org/api/sqlite.html), available without a flag since Node.js 22.13.0. No installation is required. For older Node.js versions, [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) offers a similar synchronous API. ### Example ```cjs title="index.cjs" -const mysql = require('mysql'); -const connection = mysql.createConnection({ - host: 'localhost', - user: 'dbuser', - password: 's3kreee7', - database: 'my_db', -}); +const express = require('express'); +const { DatabaseSync } = require('node:sqlite'); -connection.connect(); +const app = express(); +const db = new DatabaseSync('app.db'); -connection.query('SELECT 1 + 1 AS solution', (err, rows, fields) => { - if (err) throw err; +db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); - console.log('The solution is: ', rows[0].solution); +app.get('/users/:id', (req, res) => { + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); + res.json(user); }); -connection.end(); +app.listen(3000); ``` ```mjs title="index.mjs" -import mysql from 'mysql'; - -const connection = mysql.createConnection({ - host: 'localhost', - user: 'dbuser', - password: 's3kreee7', - database: 'my_db', -}); - -connection.connect(); - -connection.query('SELECT 1 + 1 AS solution', (err, rows, fields) => { - if (err) throw err; - - console.log('The solution is: ', rows[0].solution); -}); - -connection.end(); -``` - -## MongoDB - -**Module**: [mongodb](https://github.com/mongodb/node-mongodb-native) - -### Installation - - - -### Example (v2.\*) +import express from 'express'; +import { DatabaseSync } from 'node:sqlite'; -```js title="index.js" -const MongoClient = require('mongodb').MongoClient; +const app = express(); +const db = new DatabaseSync('app.db'); -MongoClient.connect('mongodb://localhost:27017/animals', (err, db) => { - if (err) throw err; +db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); - db.collection('mammals') - .find() - .toArray((err, result) => { - if (err) throw err; - - console.log(result); - }); +app.get('/users/:id', (req, res) => { + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); + res.json(user); }); -``` - -### Example (v3.\*) - -```js title="index.js" -const MongoClient = require('mongodb').MongoClient; - -MongoClient.connect('mongodb://localhost:27017/animals', (err, client) => { - if (err) throw err; - const db = client.db('animals'); - - db.collection('mammals') - .find() - .toArray((err, result) => { - if (err) throw err; - - console.log(result); - }); -}); +app.listen(3000); ``` -If you want an object model driver for MongoDB, look at [Mongoose](https://github.com/LearnBoost/mongoose). +```ts title="index.ts" +import express from 'express'; +import { DatabaseSync } from 'node:sqlite'; -## Neo4j - -**Module**: [neo4j-driver](https://github.com/neo4j/neo4j-javascript-driver) +interface User { + id: number; + name: string; +} -### Installation +const app = express(); +const db = new DatabaseSync('app.db'); - +db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); -### Example - -```cjs title="index.cjs" -const neo4j = require('neo4j-driver'); -const driver = neo4j.driver('neo4j://localhost:7687', neo4j.auth.basic('neo4j', 'letmein')); - -const session = driver.session(); - -session.readTransaction((tx) => { - return tx - .run('MATCH (n) RETURN count(n) AS count') - .then((res) => { - console.log(res.records[0].get('count')); - }) - .catch((error) => { - console.log(error); - }); +app.get('/users/:id', (req, res) => { + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id) as User | undefined; + res.json(user); }); -``` - -```mjs title="index.mjs" -import neo4j from 'neo4j-driver'; -const driver = neo4j.driver('neo4j://localhost:7687', neo4j.auth.basic('neo4j', 'letmein')); - -const session = driver.session(); - -session.readTransaction((tx) => { - return tx - .run('MATCH (n) RETURN count(n) AS count') - .then((res) => { - console.log(res.records[0].get('count')); - }) - .catch((error) => { - console.log(error); - }); -}); +app.listen(3000); ``` -## Oracle +## Other databases -**Module**: [oracledb](https://github.com/oracle/node-oracledb) +Every other database follows the same pattern: install the Node.js driver from npm, create a client or connection pool once when the app starts, and use it in your route handlers. Refer to the official documentation of each driver for connection options and API details: -### Installation +- PostgreSQL: [pg](https://node-postgres.com/) or [postgres](https://github.com/porsager/postgres) +- MySQL and MariaDB: [mysql2](https://sidorares.github.io/node-mysql2/docs) or [mariadb](https://mariadb.com/docs/connectors/mariadb-connector-nodejs) +- MongoDB: [mongodb](https://www.mongodb.com/docs/drivers/node/current/) +- Redis: [redis](https://redis.io/docs/latest/develop/clients/nodejs/) or [ioredis](https://github.com/redis/ioredis) +- SQL Server: [mssql](https://github.com/tediousjs/node-mssql) +- Oracle: [oracledb](https://www.npmjs.com/package/oracledb) +- Cassandra: [cassandra-driver](https://docs.datastax.com/en/developer/nodejs-driver/latest/) +- Elasticsearch: [@elastic/elasticsearch](https://www.elastic.co/docs/reference/elasticsearch/clients/javascript) -NOTE: [See installation prerequisites](https://github.com/oracle/node-oracledb#-installation). +For any database not listed here, search for its driver on the [npm](https://www.npmjs.com/) site. - +## ORMs and query builders -### Example +The example above uses a database driver directly, which is a good fit for small apps and for learning how things work. As an application grows, an ORM (object-relational mapper) or a query builder can take over concerns such as schema definitions, migrations, relations between tables, and input mapping, while reducing boilerplate. Many of them also provide type safety when used with TypeScript. -```cjs title="index.cjs" -const oracledb = require('oracledb'); -const config = { - user: '', - password: '', - connectString: 'localhost:1521/orcl', -}; - -async function getEmployee(empId) { - let conn; - - try { - conn = await oracledb.getConnection(config); - - const result = await conn.execute('select * from employees where employee_id = :id', [empId]); - - console.log(result.rows[0]); - } catch (err) { - console.log('Ouch!', err); - } finally { - if (conn) { - // conn assignment worked, need to close - await conn.close(); - } - } -} +Popular options in the Node.js ecosystem include: -getEmployee(101); -``` +- [Prisma](https://www.prisma.io/): a type-safe ORM with a schema-first workflow and built-in migrations. Supports PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, and CockroachDB. +- [Drizzle ORM](https://orm.drizzle.team/): a lightweight, TypeScript-first ORM with a SQL-like query API and a migrations toolkit. +- [Sequelize](https://sequelize.org/): a mature, promise-based ORM for PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server. +- [TypeORM](https://typeorm.io/): an ORM built around decorators and entities, supporting many SQL databases and MongoDB. +- [Mongoose](https://mongoosejs.com/): the most widely used ODM (object document mapper) for MongoDB. +- [Knex.js](https://knexjs.org/): a flexible SQL query builder with migrations support, useful when you want control over your queries without writing raw strings. +- [Kysely](https://kysely.dev/): a type-safe SQL query builder for TypeScript. -```mjs title="index.mjs" -import oracledb from 'oracledb'; - -const config = { - user: '', - password: '', - connectString: 'localhost:1521/orcl', -}; - -async function getEmployee(empId) { - let conn; - - try { - conn = await oracledb.getConnection(config); - - const result = await conn.execute('select * from employees where employee_id = :id', [empId]); - - console.log(result.rows[0]); - } catch (err) { - console.log('Ouch!', err); - } finally { - if (conn) { - // conn assignment worked, need to close - await conn.close(); - } - } -} +There is no single right choice. Query builders keep you close to SQL, while full ORMs abstract more of it away. Pick the level of abstraction that matches your team and project, and prefer tools that are actively maintained. -getEmployee(101); -``` +## Best practices -## PostgreSQL +These practices apply regardless of the database or library you choose. The snippets below use the connection pool from the [pg](https://node-postgres.com/) driver as an example, but every driver and ORM offers an equivalent. -**Module**: [pg-promise](https://github.com/vitaly-t/pg-promise) +### Create connections once and reuse them -### Installation +Opening a database connection is expensive. Most drivers provide a connection pool: a set of connections that is opened once, kept alive, and shared across requests, so each query borrows an existing connection instead of opening a new one. Create the pool (or client) once when the app starts, then reuse it in your route handlers, as the example on this page does. Never open a new connection inside a request handler. - +### Use parameterized queries -### Example +Never build queries by concatenating or interpolating user input into the query string. Doing so exposes your app to SQL injection (or the equivalent injection attack for NoSQL databases). Every driver and ORM supports parameterized queries or prepared statements, which pass user input separately from the query itself: -```js title="index.js" -const pgp = require('pg-promise')(/* options */); -const db = pgp('postgres://username:password@host:port/database'); +```js +// Vulnerable: user input becomes part of the query +const user = await pool.query(`SELECT * FROM users WHERE id = ${req.params.id}`); -db.one('SELECT $1 AS value', 123) - .then((data) => { - console.log('DATA:', data.value); - }) - .catch((error) => { - console.log('ERROR:', error); - }); +// Safe: user input is passed as a parameter +const user = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]); ``` -## Redis - -**Module**: [redis](https://github.com/mranney/node_redis) - -### Installation +Also validate user input before it reaches your queries, both for security and for data integrity. - +### Keep credentials out of your code -### Example - -```cjs title="index.cjs" -const redis = require('redis'); -const client = redis.createClient(); +Never hardcode connection strings, usernames, or passwords in your source code, and never commit them to version control. Read them from environment variables or a secrets manager instead: -client.on('error', (err) => { - console.log(`Error ${err}`); -}); - -client.set('string key', 'string val', redis.print); -client.hset('hash key', 'hashtest 1', 'some value', redis.print); -client.hset(['hash key', 'hashtest 2', 'some other value'], redis.print); - -client.hkeys('hash key', (err, replies) => { - console.log(`${replies.length} replies:`); - - replies.forEach((reply, i) => { - console.log(` ${i}: ${reply}`); - }); - - client.quit(); -}); +```js +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); ``` -```mjs title="index.mjs" -import redis from 'redis'; - -const client = redis.createClient(); - -client.on('error', (err) => { - console.log(`Error ${err}`); -}); +During development, you can keep these values in a `.env` file and load it with Node.js's built-in support for environment files, with no extra dependencies: -client.set('string key', 'string val', redis.print); -client.hset('hash key', 'hashtest 1', 'some value', redis.print); -client.hset(['hash key', 'hashtest 2', 'some other value'], redis.print); - -client.hkeys('hash key', (err, replies) => { - console.log(`${replies.length} replies:`); - - replies.forEach((reply, i) => { - console.log(` ${i}: ${reply}`); - }); - - client.quit(); -}); +```bash +node --env-file=.env index.js ``` -## SQL Server +Make sure `.env` is listed in your `.gitignore` file so it never gets committed. -**Module**: [tedious](https://github.com/tediousjs/tedious) +### Handle database errors -### Installation +Database calls fail: connections drop, queries time out, constraints are violated. In Express 5, errors thrown in async route handlers are passed to your [error-handling middleware](/en/guide/error-handling) automatically, so define one and decide there what to expose to clients. Avoid leaking internal error details, such as query text or stack traces, in API responses. - +### Cancel work when the client disconnects -### Example +Since Node.js 24.16.0, every request exposes [req.signal](https://nodejs.org/api/http.html#messagesignal), an `AbortSignal` that is aborted when the client disconnects. Pass it to any cancellation-aware API, such as a database driver that supports abort signals, so your app stops doing work nobody is waiting for: -```js title="index.js" -const Connection = require('tedious').Connection; -const Request = require('tedious').Request; - -const config = { - server: 'localhost', - authentication: { - type: 'default', - options: { - userName: 'your_username', // update me - password: 'your_password', // update me - }, - }, -}; - -const connection = new Connection(config); - -connection.on('connect', (err) => { - if (err) { - console.log(err); - } else { - executeStatement(); - } +```js +app.get('/reports', async (req, res) => { + const report = await generateReport({ signal: req.signal }); + res.json(report); }); - -function executeStatement() { - request = new Request("select 123, 'hello world'", (err, rowCount) => { - if (err) { - console.log(err); - } else { - console.log(`${rowCount} rows`); - } - connection.close(); - }); - - request.on('row', (columns) => { - columns.forEach((column) => { - if (column.value === null) { - console.log('NULL'); - } else { - console.log(column.value); - } - }); - }); - - connection.execSql(request); -} ``` -## SQLite - -**Module**: [sqlite3](https://github.com/mapbox/node-sqlite3) +### Close connections on shutdown -### Installation +When your app receives a termination signal, stop accepting new requests and close your database connections so in-flight queries can finish cleanly: - - -### Example +```js +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const server = app.listen(3000); -```js title="index.js" -const sqlite3 = require('sqlite3').verbose(); -const db = new sqlite3.Database(':memory:'); - -db.serialize(() => { - db.run('CREATE TABLE lorem (info TEXT)'); - const stmt = db.prepare('INSERT INTO lorem VALUES (?)'); - - for (let i = 0; i < 10; i++) { - stmt.run(`Ipsum ${i}`); - } - - stmt.finalize(); - - db.each('SELECT rowid AS id, info FROM lorem', (err, row) => { - console.log(`${row.id}: ${row.info}`); +process.on('SIGTERM', () => { + server.close(async () => { + await pool.end(); + process.exit(0); }); }); - -db.close(); ``` -## Elasticsearch - -**Module**: [elasticsearch](https://github.com/elastic/elasticsearch-js) - -### Installation - - - -### Example - -```cjs title="index.cjs" -const elasticsearch = require('elasticsearch'); -const client = elasticsearch.Client({ - host: 'localhost:9200', -}); - -client - .search({ - index: 'books', - type: 'book', - body: { - query: { - multi_match: { - query: 'express js', - fields: ['title', 'description'], - }, - }, - }, - }) - .then( - (response) => { - const hits = response.hits.hits; - }, - (error) => { - console.trace(error.message); - } - ); -``` - -```mjs title="index.mjs" -import elasticsearch from 'elasticsearch'; - -const client = elasticsearch.Client({ - host: 'localhost:9200', -}); - -client - .search({ - index: 'books', - type: 'book', - body: { - query: { - multi_match: { - query: 'express js', - fields: ['title', 'description'], - }, - }, - }, - }) - .then( - (response) => { - const hits = response.hits.hits; - }, - (error) => { - console.trace(error.message); - } - ); -``` +If you already use an [AbortSignal](https://nodejs.org/api/globals.html#class-abortsignal) to coordinate cancellation across your app, you can pass it to `app.listen` and abort it instead of calling `server.close()` directly. From bafbe2bd01887aeb792606dd95c8f42fdfdcbcbf Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sun, 2 Aug 2026 14:36:24 -0500 Subject: [PATCH 2/6] fixup! --- src/content/pages/en/guide/database-integration.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/content/pages/en/guide/database-integration.mdx b/src/content/pages/en/guide/database-integration.mdx index 996d1ee80a..030bfb30bb 100644 --- a/src/content/pages/en/guide/database-integration.mdx +++ b/src/content/pages/en/guide/database-integration.mdx @@ -60,7 +60,9 @@ const db = new DatabaseSync('app.db'); db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); app.get('/users/:id', (req, res) => { - const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id) as User | undefined; + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id) as + | User + | undefined; res.json(user); }); From 1f508c0f5c00c0ac23bcdedae44e899906d3da96 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 2 Oct 2026 11:29:28 -0500 Subject: [PATCH 3/6] Update src/content/pages/en/guide/database-integration.mdx Co-authored-by: Rand McKinney Signed-off-by: Sebastian Beltran --- src/content/pages/en/guide/database-integration.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/content/pages/en/guide/database-integration.mdx b/src/content/pages/en/guide/database-integration.mdx index 030bfb30bb..ff3c03292e 100644 --- a/src/content/pages/en/guide/database-integration.mdx +++ b/src/content/pages/en/guide/database-integration.mdx @@ -3,7 +3,9 @@ title: Database integration description: Learn how to connect databases to Express.js applications, when to use an ORM, and best practices for connection pooling, security, and error handling. --- -Express doesn't include database support out of the box, and it doesn't need to. Connecting a database to an Express app is just a matter of loading an appropriate Node.js driver or ORM in your app. This page shows the general pattern using SQLite as an example, points you to the official documentation for other popular databases, and covers a few practices that apply no matter which database you choose. +To connect a database to an Express app simply load the desired Node.js driver or ORM in your app. The general pattern is shown below using SQLite as an example. + +There are also a few [best practices ](#best-practices) to follow, regardless of the database you're using. ## SQLite From 429bde88d72106a9c293dcf4b5df5d40f12016f1 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 2 Oct 2026 11:34:26 -0500 Subject: [PATCH 4/6] fix lint --- src/content/pages/en/guide/database-integration.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/pages/en/guide/database-integration.mdx b/src/content/pages/en/guide/database-integration.mdx index ff3c03292e..b0b27087d1 100644 --- a/src/content/pages/en/guide/database-integration.mdx +++ b/src/content/pages/en/guide/database-integration.mdx @@ -3,7 +3,7 @@ title: Database integration description: Learn how to connect databases to Express.js applications, when to use an ORM, and best practices for connection pooling, security, and error handling. --- -To connect a database to an Express app simply load the desired Node.js driver or ORM in your app. The general pattern is shown below using SQLite as an example. +To connect a database to an Express app simply load the desired Node.js driver or ORM in your app. The general pattern is shown below using SQLite as an example. There are also a few [best practices ](#best-practices) to follow, regardless of the database you're using. From 1226b67ddfbeba424814c41d568216f6f5eeb750 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Fri, 2 Oct 2026 12:12:41 -0500 Subject: [PATCH 5/6] fix: update database integration guide with best practices and example improvements --- project-words.txt | 1 + .../pages/en/guide/database-integration.mdx | 54 +++++++++++++------ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/project-words.txt b/project-words.txt index 488de438c6..fc6c908665 100644 --- a/project-words.txt +++ b/project-words.txt @@ -62,6 +62,7 @@ Kawaii Keygrip Konnichiwa kreee +Kysely letmein linklocal llms diff --git a/src/content/pages/en/guide/database-integration.mdx b/src/content/pages/en/guide/database-integration.mdx index b0b27087d1..ef28474446 100644 --- a/src/content/pages/en/guide/database-integration.mdx +++ b/src/content/pages/en/guide/database-integration.mdx @@ -5,7 +5,7 @@ description: Learn how to connect databases to Express.js applications, when to To connect a database to an Express app simply load the desired Node.js driver or ORM in your app. The general pattern is shown below using SQLite as an example. -There are also a few [best practices ](#best-practices) to follow, regardless of the database you're using. +There are also a few [best practices](#best-practices) to follow, regardless of the database you're using. ## SQLite @@ -13,6 +13,8 @@ Node.js includes a built-in SQLite client, [node:sqlite](https://nodejs.org/api/ ### Example +This example creates a sample user that you can retrieve at `http://localhost:3000/users/1`. It uses a synchronous SQLite API for simplicity: long-running queries block the thread handling requests. For workloads with long-running queries, consider moving database work to a worker thread or using an asynchronous database driver. + ```cjs title="index.cjs" const express = require('express'); const { DatabaseSync } = require('node:sqlite'); @@ -20,10 +22,15 @@ const { DatabaseSync } = require('node:sqlite'); const app = express(); const db = new DatabaseSync('app.db'); -db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); +db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)'); +db.prepare('INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO NOTHING').run(1, 'Alice'); +const findUser = db.prepare('SELECT id, name FROM users WHERE id = ?'); app.get('/users/:id', (req, res) => { - const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); + const user = findUser.get(req.params.id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } res.json(user); }); @@ -37,10 +44,15 @@ import { DatabaseSync } from 'node:sqlite'; const app = express(); const db = new DatabaseSync('app.db'); -db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); +db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)'); +db.prepare('INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO NOTHING').run(1, 'Alice'); +const findUser = db.prepare('SELECT id, name FROM users WHERE id = ?'); app.get('/users/:id', (req, res) => { - const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); + const user = findUser.get(req.params.id); + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } res.json(user); }); @@ -59,12 +71,15 @@ interface User { const app = express(); const db = new DatabaseSync('app.db'); -db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)'); +db.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)'); +db.prepare('INSERT INTO users (id, name) VALUES (?, ?) ON CONFLICT(id) DO NOTHING').run(1, 'Alice'); +const findUser = db.prepare('SELECT id, name FROM users WHERE id = ?'); app.get('/users/:id', (req, res) => { - const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id) as - | User - | undefined; + const user = findUser.get(req.params.id) as User | undefined; + if (!user) { + return res.status(404).json({ error: 'User not found' }); + } res.json(user); }); @@ -96,33 +111,41 @@ Popular options in the Node.js ecosystem include: - [Drizzle ORM](https://orm.drizzle.team/): a lightweight, TypeScript-first ORM with a SQL-like query API and a migrations toolkit. - [Sequelize](https://sequelize.org/): a mature, promise-based ORM for PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server. - [TypeORM](https://typeorm.io/): an ORM built around decorators and entities, supporting many SQL databases and MongoDB. -- [Mongoose](https://mongoosejs.com/): the most widely used ODM (object document mapper) for MongoDB. +- [Mongoose](https://mongoosejs.com/): an ODM (object document mapper) for MongoDB. - [Knex.js](https://knexjs.org/): a flexible SQL query builder with migrations support, useful when you want control over your queries without writing raw strings. - [Kysely](https://kysely.dev/): a type-safe SQL query builder for TypeScript. -There is no single right choice. Query builders keep you close to SQL, while full ORMs abstract more of it away. Pick the level of abstraction that matches your team and project, and prefer tools that are actively maintained. +Each ORM has its own API, supported databases, and approach to schemas, migrations, and queries. Features and setup requirements vary, so consult each project's documentation before choosing one. Query builders keep you close to SQL, while full ORMs abstract more of it away. Pick the level of abstraction that matches your team and project, and prefer tools that are actively maintained. ## Best practices -These practices apply regardless of the database or library you choose. The snippets below use the connection pool from the [pg](https://node-postgres.com/) driver as an example, but every driver and ORM offers an equivalent. +Adapt these practices to the database and library you choose. The snippets below illustrate separate parts of an application. The PostgreSQL snippets assume you have installed the [pg](https://node-postgres.com/) driver and created a shared pool: + +```js +import { Pool } from 'pg'; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +``` ### Create connections once and reuse them -Opening a database connection is expensive. Most drivers provide a connection pool: a set of connections that is opened once, kept alive, and shared across requests, so each query borrows an existing connection instead of opening a new one. Create the pool (or client) once when the app starts, then reuse it in your route handlers, as the example on this page does. Never open a new connection inside a request handler. +Opening a database connection is expensive. Many drivers provide a connection pool that creates connections as needed and reuses them across requests. Create the pool (or client) once when the app starts. Acquire connections from that shared pool as needed in your route handlers, for example for a transaction, and always release them in a `finally` block. For individual queries, `pool.query()` in `pg` handles acquiring and releasing a connection for you. ### Use parameterized queries -Never build queries by concatenating or interpolating user input into the query string. Doing so exposes your app to SQL injection (or the equivalent injection attack for NoSQL databases). Every driver and ORM supports parameterized queries or prepared statements, which pass user input separately from the query itself: +Never build SQL queries by concatenating or interpolating user input into the query string. Doing so exposes your app to SQL injection. Use your driver's parameterized queries or prepared statements to pass user input separately from the query itself. ```js // Vulnerable: user input becomes part of the query const user = await pool.query(`SELECT * FROM users WHERE id = ${req.params.id}`); +``` +```js // Safe: user input is passed as a parameter const user = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]); ``` -Also validate user input before it reaches your queries, both for security and for data integrity. +Also validate user input before it reaches your queries, both for security and for data integrity. For NoSQL databases, construct queries using the driver's API and validate the types, fields, and operators you accept. Do not pass user-supplied query objects directly to the database. ### Keep credentials out of your code @@ -160,7 +183,6 @@ app.get('/reports', async (req, res) => { When your app receives a termination signal, stop accepting new requests and close your database connections so in-flight queries can finish cleanly: ```js -const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const server = app.listen(3000); process.on('SIGTERM', () => { From b40f8e74db241104ee02630a287d1ea92b5ec638 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Sat, 24 Oct 2026 13:56:43 -0500 Subject: [PATCH 6/6] docs: update Node.js version for req.signal --- src/content/pages/en/guide/database-integration.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/pages/en/guide/database-integration.mdx b/src/content/pages/en/guide/database-integration.mdx index ef28474446..b80ed4d966 100644 --- a/src/content/pages/en/guide/database-integration.mdx +++ b/src/content/pages/en/guide/database-integration.mdx @@ -169,7 +169,7 @@ Database calls fail: connections drop, queries time out, constraints are violate ### Cancel work when the client disconnects -Since Node.js 24.16.0, every request exposes [req.signal](https://nodejs.org/api/http.html#messagesignal), an `AbortSignal` that is aborted when the client disconnects. Pass it to any cancellation-aware API, such as a database driver that supports abort signals, so your app stops doing work nobody is waiting for: +Since Node.js 24.20.0, every request exposes [req.signal](https://nodejs.org/api/http.html#messagesignal), an `AbortSignal` that is aborted when the client disconnects. Pass it to any cancellation-aware API, such as a database driver that supports abort signals, so your app stops doing work nobody is waiting for: ```js app.get('/reports', async (req, res) => {