15 questions found
What is the difference between a relational (SQL) database and a NoSQL database, and how does this affect a Node.js application's data layer?
Beginner
A relational database (PostgreSQL, MySQL) organizes data into structured tables with a fixed schema and enforces relationships via foreign keys, well suited for data with clear, stable relationships and where transactional consistency is critical. A NoSQL database (MongoDB, DynamoDB) stores more flexible, often document- or key-value-shaped data without a rigid enforced schema, favoring scalability and adaptability to changing data shapes over strict relational integrity -- Node.js applications commonly use a driver or ORM matched to whichever model suits the application's actual data access patterns.
// SQL (via a query builder)
const users = await db('users').where({ active: true }).select('id', 'name');
// NoSQL (MongoDB)
const users = await db.collection('users').find({ active: true }).toArray();
Real-world example
A financial application choosing between the two picks PostgreSQL for its core ledger data, where strict transactional consistency and relational integrity between accounts and transactions is non-negotiable, while using MongoDB for a separate feature storing flexible, frequently-changing user-preference documents.
Common follow-ups: In what scenario would a team choose to use both a relational and a NoSQL database within the same application (polyglot persistence)?;How does the lack of a rigid schema in NoSQL shift schema-validation responsibility onto the application code?
Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize);Architecture & Design Patterns
What is a database connection pool, and why does a Node.js application need one rather than opening a new connection per request?
Intermediate
A connection pool maintains a set of already-established database connections that are reused across requests rather than opening and closing a new (relatively expensive to establish) connection for every single database operation -- without pooling, connection setup overhead would dominate request latency, and a sudden traffic spike could exhaust the database's own maximum connection limit far more easily.
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 20 });
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
Real-world example
An API configures its PostgreSQL connection pool with a maximum of 20 connections per instance, calculated so that across all running instances the total never exceeds the database server's own configured connection limit, preventing 'too many connections' errors under load.
Common follow-ups: How do you determine an appropriate maximum pool size given both the database's connection limit and the number of running application instances?;What happens to a request if it needs a connection but the pool is already fully checked out?
Performance Optimization & Profiling;Cloud & DevOps
What is a database transaction, and how would you implement one in Node.js to ensure multiple related writes either all succeed or all fail together?
Advanced
A transaction groups multiple database operations so they're treated as a single atomic unit -- either every operation within it commits successfully, or if any fails, the entire transaction is rolled back, leaving the database in its original consistent state, essential whenever multiple related writes (like debiting one account and crediting another) must never be left partially applied.
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('UPDATE accounts SET balance = balance - 100 WHERE id = $1', [fromId]);
await client.query('UPDATE accounts SET balance = balance + 100 WHERE id = $1', [toId]);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
Real-world example
A money-transfer feature wraps both the debit and credit operations in a single database transaction, guaranteeing that if the credit operation fails for any reason after the debit already succeeded, the entire transfer rolls back rather than leaving money deducted from one account without appearing in the other.
Common follow-ups: What are the ACID properties, and which one specifically does a transaction most directly guarantee?;How do transactions behave differently in a NoSQL database like MongoDB compared to a traditional relational database?
Error Handling;Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize)
What is SQL injection, and how does using parameterized queries in Node.js prevent it?
Intermediate
SQL injection occurs when untrusted user input is concatenated directly into a SQL query string, letting an attacker craft input that changes the query's actual meaning (like appending "OR 1=1" to bypass a WHERE clause, or appending a second malicious statement). Parameterized queries send the query structure and the user-supplied values separately to the database driver, which treats values strictly as data, never as executable SQL syntax, making injection through that input impossible regardless of what characters it contains.
// Vulnerable: user input concatenated directly into the query
const result = await pool.query(`SELECT * FROM users WHERE email = '${userInput}'`);
// Safe: parameterized query, value passed separately
const result = await pool.query('SELECT * FROM users WHERE email = $1', [userInput]);
Real-world example
A security audit finds a login endpoint vulnerable to SQL injection through its unescaped, string-concatenated email lookup query; switching to a parameterized query with the email passed as a separate bound parameter eliminates the vulnerability entirely without changing the query's logic.
Common follow-ups: Why doesn't simply escaping quote characters manually provide the same guarantee as a properly parameterized query?;Do NoSQL databases have an equivalent injection risk, and if so, what does it look like?
Security;Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize)
What is database indexing, and how does an index improve query performance in a Node.js application's database layer?
Advanced
An index is a separate data structure (commonly a B-tree) that the database maintains alongside a table, allowing it to look up rows matching a specific column value without scanning every row in the table sequentially -- dramatically speeding up queries filtering or sorting on indexed columns, at the cost of additional storage space and slightly slower write performance, since every index must also be updated on every insert or update.
-- Without an index, this query scans the entire orders table
SELECT * FROM orders WHERE customer_id = 12345;
-- Creating an index dramatically speeds up that exact query pattern
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
Real-world example
An orders table growing to millions of rows starts causing a 'get customer's orders' query to take several seconds; adding an index on the customer_id column reduces that same query to milliseconds by letting the database jump directly to matching rows instead of scanning the entire table.
Common follow-ups: How do you identify which columns actually need an index versus adding indexes speculatively everywhere?;What's the tradeoff of having too many indexes on a frequently-written-to table?
Performance Optimization & Profiling;SQL Queries
What is database normalization, and what specific problems can over-normalization or under-normalization cause?
Intermediate
Normalization organizes relational data to minimize redundancy, typically by splitting data into multiple related tables connected via foreign keys rather than repeating the same information across many rows. Under-normalization (excessive duplication) risks data inconsistency when the same fact needs updating in many places. Over-normalization can require excessive joins across many tables for even simple queries, hurting both query performance and code complexity, which is why some applications deliberately denormalize specific frequently-read data for performance.
-- Normalized: customer info stored once, referenced by ID
CREATE TABLE customers (id SERIAL PRIMARY KEY, name TEXT, email TEXT);
CREATE TABLE orders (id SERIAL PRIMARY KEY, customer_id INT REFERENCES customers(id), total DECIMAL);
-- vs. denormalized: customer name duplicated on every order row for fast reads
Real-world example
A reporting dashboard querying heavily normalized order data across five joined tables was too slow for real-time use; the team deliberately denormalized a subset of frequently-needed fields into a dedicated reporting table, trading some redundancy for significantly faster read performance.
Common follow-ups: How do you decide which specific data is worth denormalizing for performance versus keeping fully normalized?;What's the risk of denormalized data becoming inconsistent with its normalized source of truth?
SQL Queries;Performance Optimization & Profiling
What is database replication, and how does a Node.js application typically route reads to replicas while sending writes to a primary?
Advanced
Replication maintains one or more read-only copies (replicas) of a primary database, continuously synchronized with the primary's writes -- read-heavy applications route SELECT queries to replicas (distributing read load across multiple database servers) while all writes go exclusively to the primary, since replicas typically can't accept writes and there's a brief replication lag before a replica reflects the primary's most recent writes.
const primaryPool = new Pool({ connectionString: PRIMARY_DB_URL });
const replicaPool = new Pool({ connectionString: REPLICA_DB_URL });
async function getUser(id) { return replicaPool.query('SELECT * FROM users WHERE id = $1', [id]); }
async function updateUser(id, data) { return primaryPool.query('UPDATE users SET ... WHERE id = $1', [id]); }
Real-world example
A high-traffic content platform routes all its read-heavy product-listing queries to a pool of three read replicas while sending all cart and checkout writes to the single primary database, significantly reducing load on the primary and allowing read capacity to scale independently of write capacity.
Common follow-ups: What problems can arise from replication lag if an application immediately reads data right after writing it?;How do you handle read-your-own-writes consistency when a user needs to see their own recent write immediately?
Performance Optimization & Profiling;Architecture & Design Patterns
What is database migration, and why is it important to manage schema changes through versioned migration files rather than manual changes?
Intermediate
A migration is a versioned, incremental script describing a specific schema change (adding a column, creating a table) that can be applied and, ideally, reversed in a controlled, repeatable, and auditable way -- managing schema changes through migration files (checked into version control alongside application code) rather than manual database edits ensures every environment's schema stays in a known, consistent, and reproducible state, and lets the team track exactly when and why each change was made.
// Example migration file (using a tool like Knex or Sequelize)
exports.up = async (knex) => {
await knex.schema.table('users', (table) => {
table.string('phone_number').nullable();
});
};
exports.down = async (knex) => {
await knex.schema.table('users', (table) => table.dropColumn('phone_number'));
};
Real-world example
A team deploying a new feature requiring an additional database column writes a migration file rather than manually running an ALTER TABLE statement against production, ensuring the exact same schema change is automatically and consistently applied across development, staging, and production databases.
Common follow-ups: How do you handle a migration that needs to run against a production database with live traffic and zero downtime?;What's the risk of a migration that isn't properly reversible via a working 'down' script?
Git & Project Management;Cloud & DevOps
What is optimistic concurrency control, and how would you implement it to prevent lost updates when two users edit the same record simultaneously?
Advanced
Optimistic concurrency control lets multiple users read and attempt to update the same record without locking it, but detects conflicting concurrent updates at write time (commonly via a version number or timestamp column that must match the value originally read) -- if another update has happened in between, the write is rejected and the conflicting user must reload the current data and retry, preventing one user's update from silently overwriting another's without either party realizing it.
async function updateDocument(id, expectedVersion, newContent) {
const result = await db.documents.updateOne(
{ _id: id, version: expectedVersion },
{ $set: { content: newContent }, $inc: { version: 1 } }
);
if (result.modifiedCount === 0) throw new Error('Conflict: document was modified by someone else');
}
Real-world example
A collaborative document-editing feature includes a version number with every edit request; if a user submits an edit based on an outdated version (because someone else already saved a change), the update is rejected with a conflict error rather than silently overwriting the other user's changes.
Common follow-ups: How does optimistic concurrency control differ from pessimistic locking, and when is each more appropriate?;What should the user experience be when a conflict is detected -- how do you help them resolve it?
Error Handling;Design Patterns in JavaScript
What is the N+1 query problem, and how does it commonly arise in a Node.js application using an ORM?
Intermediate
The N+1 query problem occurs when code fetches a list of N parent records with one query, then issues a separate additional query for each parent's related child data inside a loop, resulting in N+1 total queries instead of a single efficient join or batched query -- commonly introduced accidentally through an ORM's lazy-loading behavior, where accessing a related field triggers an implicit, easy-to-miss query.
// N+1 problem: one query for users, then one additional query per user for their orders
const users = await User.findAll();
for (const user of users) {
const orders = await user.getOrders(); // triggers a separate query, N times
}
// Fixed: a single query with eager loading
const users = await User.findAll({ include: [Order] });
Real-world example
A dashboard listing 100 users along with each user's most recent order was issuing 101 total database queries until the team added eager loading via the ORM's include option, collapsing it down to a single efficient join query.
Common follow-ups: How would you detect an N+1 query problem in an existing codebase before it becomes a production performance issue?;Does the N+1 problem apply to raw SQL queries as well, or is it specifically an ORM concern?
Databases & ORMs (MongoDB/Mongoose
SQL/Sequelize);Performance Optimization & Profiling