Database Management

Chapter 9
Database Integration for Full-Stack Applications

Certificate in Full stack programming
Database Management   •   Certificate

Database Integration for Full-Stack Applications

Introduction

A database becomes useful to a full-stack application only when application code can reliably communicate with it. Database integration is the set of design and programming practices that connect a server-side application to a database, execute safe operations, transform results into application objects, and manage changes to the database structure over time.

In a typical web application, a browser sends an HTTP request to a server. The server authenticates the user, validates the request, applies business rules, accesses persistent data, and returns an HTTP response. The database is therefore part of a larger system rather than an isolated component. Poor integration can cause data loss, security vulnerabilities, connection exhaustion, slow response times, and difficult deployments.

This chapter examines drivers, database APIs, connection pools, parameterized queries, repositories, migrations, and object-relational mapping (ORM). It also explains SQL injection and shows how layered design makes database access safer and easier to maintain.

Learning Objectives

By the end of this chapter, a learner should be able to:

  • explain how server-side applications communicate with relational databases;
  • distinguish database drivers, APIs, repositories, and ORMs;
  • configure and use a connection pool responsibly;
  • write parameterized queries that prevent SQL injection;
  • design a repository layer for common data operations;
  • use migrations to manage database schema changes;
  • describe the advantages and limitations of ORM tools; and
  • apply secure, reliable database integration practices in a full-stack application.

1. The Application–Database Boundary

A server-side application normally contains several logical responsibilities:

  1. Transport handling receives HTTP requests and produces responses.
  2. Application or service logic applies business rules.
  3. Data access communicates with the database.
  4. Persistence stores and retrieves records.

A simplified request flow is:

Client → HTTP route/controller → service logic → repository/data access → database
       ← HTTP response       ← domain result  ← query result       ←

The database boundary is important because it separates two different models. Application code works with objects, functions, and domain concepts such as Customer or Order. A relational database works with tables, rows, columns, keys, constraints, and SQL statements. Integration code translates between these models.

A well-designed boundary has four characteristics:

  • Correctness: data is read and written according to business rules.
  • Security: untrusted input cannot change the meaning of a query.
  • Reliability: failures are detected, handled, and logged appropriately.
  • Maintainability: database-specific code is concentrated in predictable locations.

A database should not normally be accessed directly from browser code. Exposing database credentials to a browser would allow users to bypass server-side authorization and potentially alter or inspect data. The server acts as the controlled intermediary.

2. Drivers and Database APIs

A database driver is software that implements the communication protocol required by a particular database system. For example, a PostgreSQL driver understands how to send PostgreSQL commands and interpret PostgreSQL responses. A MySQL driver performs the equivalent role for MySQL.

A driver typically provides functions or methods to:

  • open and close connections;
  • authenticate with the database;
  • send SQL statements;
  • bind parameter values;
  • receive rows, affected-row counts, and error information; and
  • begin, commit, or roll back transactions.

A database API is the programming interface exposed to application code. In JavaScript, a driver may expose asynchronous functions such as connect(), query(), and release(). In Python, a driver may follow the DB-API style. The exact syntax varies, but the underlying responsibilities are similar.

Example using a Node.js-style PostgreSQL interface:

const result = await pool.query(
  'SELECT id, name FROM customers WHERE id = $1',
  [customerId]
);

The SQL statement and its value are supplied separately. The driver sends the statement structure and binds the value safely.

Application configuration should normally be supplied through environment variables or a secret-management service rather than hard-coded in source code:

DB_HOST=localhost
DB_PORT=5432
DB_NAME=shop
DB_USER=app_user
DB_PASSWORD=secret-managed-outside-source

Production systems should use a database account with only the permissions required by the application. An account that can read and write application tables does not usually need permission to create operating-system users or administer unrelated databases.

3. Connection Management and Connection Pools

A database connection is a live communication channel between an application process and a database server. Establishing a connection may involve network communication, authentication, encryption negotiation, and session setup. Creating a new connection for every request is therefore inefficient and can overwhelm the database.

A connection pool maintains a controlled collection of reusable connections. When a request needs database access, it borrows an available connection, uses it, and returns it to the pool.

The basic lifecycle is:

request begins → acquire connection → execute work → release connection → request ends

A pool commonly defines:

  • minimum size: connections kept ready, if supported;
  • maximum size: the greatest number of simultaneous connections;
  • acquisition timeout: how long a request waits for an available connection;
  • idle timeout: how long an unused connection may remain open; and
  • lifetime limits: conditions for replacing old connections.

A pool is not an unlimited performance solution. If several application instances each have a pool of 20 connections, ten instances may attempt to use 200 database connections. Pool sizing must therefore consider database capacity, expected concurrency, query duration, and the number of application instances.

A common programming error is failing to release a borrowed connection when an exception occurs. The safe pattern uses a finally block or an equivalent automatic resource-management mechanism:

const client = await pool.connect();
try {
  const result = await client.query(
    'SELECT id, total FROM orders WHERE customer_id = $1',
    [customerId]
  );
  return result.rows;
} finally {
  client.release();
}

For a single independent query, a pool-level query method may acquire and release a connection automatically. A manually acquired connection is required when several statements must use the same connection, especially inside a transaction.

4. Queries, Parameters, and SQL Injection

SQL injection occurs when untrusted input is combined with SQL text in a way that allows the input to alter the query’s structure. Consider this unsafe construction:

const sql = "SELECT * FROM users WHERE email = '" + email + "'";

If email contains SQL syntax rather than an ordinary address, the resulting statement may perform an unintended search or operation. The fundamental mistake is treating data as part of the command language.

Parameterized Queries

A parameterized query keeps SQL structure separate from data values:

const result = await pool.query(
  'SELECT id, email FROM users WHERE email = $1',
  [email]
);

The placeholder $1 represents a value. The driver sends the value through the database protocol as data, not as SQL syntax. Other systems use ?, :name, or another placeholder style.

Parameterized queries should be used for all externally influenced values, including values received from forms, URLs, cookies, headers, and other services. They protect values, not arbitrary SQL identifiers. A table name, column name, or sort direction generally cannot be supplied as an ordinary value parameter. If such elements must be selected dynamically, use an allowlist:

const allowedSorts = {
  newest: 'created_at DESC',
  name: 'name ASC'
};
const orderBy = allowedSorts[sortKey] ?? allowedSorts.newest;
const sql = `SELECT id, name FROM products ORDER BY ${orderBy}`;

The interpolated text comes only from a fixed server-side allowlist. User input is never inserted directly.

Other defenses complement parameterization:

  • validate input according to its expected type and range;
  • apply authorization checks before performing sensitive operations;
  • use least-privilege database accounts;
  • avoid returning unnecessary error details to clients;
  • keep database software and drivers patched; and
  • monitor suspicious query patterns.

Input validation is valuable, but it is not a replacement for parameterized queries. A validation rule may be incomplete; parameter binding addresses the command/data boundary directly.

5. Transactions and Consistency

A transaction groups related database operations into one logical unit. Its usual properties are summarized by ACID:

  • Atomicity: all operations succeed, or the transaction is rolled back.
  • Consistency: declared rules remain satisfied after completion.
  • Isolation: concurrent transactions are controlled according to the database’s isolation level.
  • Durability: committed changes survive an appropriate system failure.

For example, transferring money between two accounts requires both a debit and a credit. If the debit succeeds but the credit fails, the transaction must roll back the debit.

const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query(
    'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
    [amount, fromId]
  );
  await client.query(
    'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
    [amount, toId]
  );
  await client.query('COMMIT');
} catch (error) {
  await client.query('ROLLBACK');
  throw error;
} finally {
  client.release();
}

The application should also verify business conditions, such as whether the source account exists and has sufficient funds. Database constraints provide an additional line of defense, but transaction logic must still be designed carefully.

Transactions should generally be short. Holding locks while waiting for network calls or user input increases contention and the risk of deadlocks.

6. The Repository Pattern

A repository is an application component that encapsulates persistence operations for a domain concept. Instead of placing SQL in every controller, the application can expose methods such as:

userRepository.findById(id)
userRepository.create(data)
userRepository.updateEmail(id, email)

A repository can centralize:

  • SQL statements and parameter binding;
  • conversion between database rows and application objects;
  • common filtering and pagination rules;
  • transaction participation; and
  • database-specific error translation.

A repository is not necessarily a complete business service. For example, deciding whether a customer is eligible for a discount belongs to service logic, while retrieving the customer record belongs to data access.

A useful separation is:

Controller: HTTP concerns and request validation
Service: business rules and transaction coordination
Repository: persistence operations
Driver: database protocol and low-level communication

This separation improves testing. Service logic can be tested with a fake repository, while repository tests can use a test database to verify SQL behavior.

Repositories should avoid returning raw, uncontrolled database objects when doing so exposes internal columns or encourages callers to depend on storage details. Explicit mapping creates a stable application interface.

7. Schema Migrations

A migration is a versioned, executable change to database structure or controlled reference data. Typical migrations create tables, add columns, create indexes, or modify constraints.

A migration tool usually records applied versions in a metadata table. When deployment occurs, the tool compares available migration files with recorded versions and applies pending changes in order.

A conceptual migration might contain:

CREATE TABLE customers (
  id BIGSERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

A later migration might add a status column:

ALTER TABLE customers
ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'active';

Good migration practices include:

  • keep migrations ordered and immutable after release;
  • make changes reviewable in version control;
  • define constraints explicitly;
  • consider how large tables will be affected;
  • separate destructive changes from application changes when necessary; and
  • test upgrades on a production-like database.

A schema change may require a multi-step deployment. For example, to rename a column safely, an application can first write to both old and new columns, backfill existing records, switch reads to the new column, and remove the old column in a later release. This approach reduces incompatibility between application versions during rolling deployment.

Migrations differ from ad hoc manual edits because they provide a repeatable history. They do not eliminate the need for backups, testing, or rollback planning. Some data transformations cannot be safely reversed, so a “down” migration may not restore the original data.

8. Object-Relational Mapping

An object-relational mapper (ORM) maps application objects and relationships to relational tables. Instead of writing every SQL statement manually, a developer may write code such as:

const user = await User.findOne({ where: { email } });

ORMs commonly provide:

  • model definitions;
  • relationship mappings;
  • query construction;
  • validation hooks;
  • transaction support; and
  • migration utilities.

ORM advantages include faster development for common CRUD operations, consistent conventions, and reduced repetitive SQL. They can also make simple relationships accessible to developers who are still learning SQL.

ORM limitations are equally important. A query that looks simple in application code may generate inefficient SQL. Automatic loading of related records can create the N+1 query problem, in which one query loads a list and an additional query is issued for each item. ORMs may also obscure database-specific features, produce difficult-to-optimize queries, or encourage a false belief that database knowledge is unnecessary.

A practical approach is to use an ORM for routine operations while retaining the ability to inspect generated SQL and write explicit queries for complex reporting, bulk operations, or performance-sensitive paths. Whether SQL is written by a developer or generated by an ORM, parameter binding and authorization remain necessary.

9. Worked Application: A Course-Enrollment Service

The following is an illustrative case, not a report of a real organization.

Background and Problem

A small training provider is building a course-enrollment application. Students browse courses, submit enrollment requests, and view their current enrollments. The first prototype stores data in memory. It loses records whenever the server restarts and allows duplicate enrollments.

Analysis

The system requires three relational entities:

  • students, identified by id;
  • courses, identified by id; and
  • enrollments, linking students and courses.

An enrollment is a many-to-many relationship between students and courses. A relational design represents it with a junction table. A uniqueness constraint on (student_id, course_id) prevents duplicate enrollment even if two requests arrive concurrently.

Approach and Solution

A migration creates the relationship:

CREATE TABLE enrollments (
  student_id BIGINT NOT NULL REFERENCES students(id),
  course_id BIGINT NOT NULL REFERENCES courses(id),
  enrolled_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (student_id, course_id)
);

A repository method uses a parameterized insert:

async function enroll(studentId, courseId) {
  const result = await pool.query(
    `INSERT INTO enrollments (student_id, course_id)
     VALUES ($1, $2)
     ON CONFLICT (student_id, course_id) DO NOTHING
     RETURNING student_id, course_id, enrolled_at`,
    [studentId, courseId]
  );
  return result.rows[0] ?? null;
}

The service validates that the student and course exist and that the course is open for enrollment. The repository performs the persistence operation. The controller converts the result into an HTTP response, such as 201 Created for a new enrollment or 409 Conflict when a business policy treats an existing enrollment as a conflict.

Outcome

The application now has durable storage, database-enforced uniqueness, controlled access to SQL, and a clear division of responsibilities. A migration allows the same schema to be created in development, testing, and production. The pool reuses connections instead of opening one for each request.

Learning Points

The case demonstrates that reliable integration is not merely a matter of connecting a driver. Correctness depends on constraints, application rules, transaction design, parameter binding, and deployment practices working together.

10. Errors, Testing, and Observability

Database failures should be classified rather than treated as identical. Examples include:

  • validation failures caused by invalid client data;
  • authorization failures;
  • unique or foreign-key constraint violations;
  • temporary connection or network failures; and
  • programming errors such as malformed SQL.

A client should receive an appropriate, non-sensitive response. Internal logs may contain a correlation identifier, operation name, duration, and database error category, but should not expose passwords, access tokens, or unnecessary personal data.

Testing should include:

  • unit tests for service rules;
  • repository tests against a disposable or dedicated test database;
  • migration tests from an empty schema and from a realistic prior version;
  • integration tests covering HTTP, service, repository, and database behavior; and
  • performance tests for important queries and expected concurrency.

Useful observability measures include query duration, error rate, pool utilization, waiting time for connections, transaction rollback rate, and slow-query frequency. SQL logs must be designed carefully so that parameter values containing personal or confidential data are not recorded unnecessarily.

11. Governance, Privacy, and Contemporary Practice

Database integration has ethical and legal implications. Applications should collect only data necessary for a legitimate purpose, restrict access according to role, protect data in transit and at rest, and define retention and deletion procedures. Personally identifiable information should not be copied into development logs or test environments without appropriate protection.

Backups must be protected because they are additional copies of sensitive data. Access should be auditable, and administrative privileges should be separated from ordinary application privileges.

Contemporary systems increasingly use managed database services, automated migration pipelines, read replicas, caching, and observability platforms. These tools can improve availability and performance but introduce operational dependencies and possible data-transfer concerns. AI-assisted programming can generate repository code or SQL, but generated code must be reviewed for injection risks, authorization errors, inefficient queries, incorrect migrations, and exposure of confidential schema or data. An AI tool does not replace testing or professional accountability.

12. Best-Practice Checklist

Before releasing a database-integrated feature, verify that:

  • database credentials are externalized and protected;
  • the application uses a least-privilege account;
  • connections are pooled and reliably released;
  • all untrusted values use parameterized queries;
  • dynamic identifiers use strict allowlists;
  • transactions cover related operations that must succeed together;
  • repositories or equivalent data-access boundaries are consistent;
  • schema changes are versioned as migrations;
  • indexes and constraints support the intended access patterns;
  • errors do not reveal secrets or internal implementation details;
  • logs and backups are handled according to privacy requirements; and
  • migrations, queries, failure paths, and concurrency behavior are tested.

Chapter Summary

Database integration connects server-side application logic with persistent data. Drivers implement database communication, while APIs expose that communication to application code. Connection pools improve efficiency by reusing a controlled number of connections, but connections must always be released.

Parameterized queries separate SQL structure from data and are the primary defense against SQL injection. Input validation, authorization, least privilege, secure error handling, and monitoring provide additional protection. Transactions preserve the integrity of related operations, while repositories organize persistence logic and reduce duplication. Migrations provide a repeatable history of schema changes. ORMs can accelerate common development tasks, but developers must understand generated SQL and recognize problems such as inefficient queries and N+1 loading.

Professional database integration combines technical design with testing, observability, privacy, governance, and careful deployment. The objective is not merely to make a query run, but to create a secure, reliable, maintainable boundary between application behavior and stored data.

Key Terms

Driver: Software that implements communication with a particular database system.

Database API: Programming interface used by an application to perform database operations.

Connection: A live communication channel between an application and a database.

Connection pool: A managed set of reusable database connections.

Parameterized query: A query whose data values are supplied separately from SQL text.

SQL injection: An attack in which input changes the intended meaning of SQL.

Transaction: A logical unit of database work treated as a coordinated operation.

ACID: Atomicity, consistency, isolation, and durability properties of transactions.

Repository: A component that encapsulates persistence operations for a domain concept.

Migration: A versioned, executable change to database schema or controlled data.

ORM: Software that maps application objects and relationships to relational structures.

Constraint: A database rule that restricts invalid or inconsistent data.

Foreign key: A column or set of columns referencing a key in another table.

Least privilege: Granting only the permissions required for a task.

Allowlist: A set of explicitly approved values or options.

N+1 query problem: Inefficient execution involving one initial query followed by one query per result item.

Repository pattern: A design approach that separates persistence operations from other application layers.

Schema: The structure and rules defining database objects and relationships.

Review Questions

Short Answer Questions

  1. What is the role of a database driver?
  2. Why are connection pools used in server-side applications?
  3. How does a parameterized query reduce SQL-injection risk?
  4. What is the purpose of a migration?
  5. State two limitations of using an ORM.

Long Answer Questions

  1. Explain the flow of a request from an HTTP controller to a relational database and identify the responsibility of each layer.
  2. Describe connection-pool management, including acquisition, release, timeouts, and pool sizing concerns.
  3. Explain transactions and the ACID properties using a money-transfer or enrollment example.
  4. Compare handwritten SQL, repositories, and ORMs in terms of control, productivity, maintainability, and performance.
  5. Describe a safe process for introducing a schema change during a rolling application deployment.

Analytical / Application Questions

  1. A developer builds SQL by concatenating a user-provided search term and also allows the user to select a sort column. Identify the risks and design a safe implementation.
  2. An application has ten server instances, each configured with a maximum pool size of 20, while the database permits only 120 application connections. Analyze the capacity problem and recommend configuration or architectural changes.
  3. An enrollment endpoint sometimes creates duplicate enrollments when two requests arrive simultaneously. Propose a solution using application logic, database constraints, and, where appropriate, transactions. Explain why each part is needed.

Multiple Choice Questions with Answers

Question 1

What is the primary purpose of a database driver?

A. To render database records in a browser

B. To implement communication between application code and a database system

C. To replace all application business rules

D. To encrypt every user document automatically

Correct Answer: B. To implement communication between application code and a database system

Question 2

What should an application do after manually borrowing a connection from a pool?

A. Keep it permanently for faster future requests

B. Release it in a finally or equivalent cleanup block

C. Send it to the browser

D. Create a second pool from it

Correct Answer: B. Release it in a finally or equivalent cleanup block

Question 3

Which practice most directly prevents user input from changing SQL structure?

A. Concatenating strings carefully

B. Using longer variable names

C. Using parameterized queries

D. Printing the query before execution

Correct Answer: C. Using parameterized queries

Question 4

Why might a dynamic sort option require an allowlist?

A. Column names and sort directions are not generally ordinary value parameters

B. Allowlisting increases database connection count

C. Sort options cannot be represented in SQL

D. Parameters work only for passwords

Correct Answer: A. Column names and sort directions are not generally ordinary value parameters

Question 5

Which database constraint can prevent duplicate student-course pairs?

A. A primary key or unique constraint on the pair

B. A default timestamp alone

C. A nullable text column

D. A connection timeout

Correct Answer: A. A primary key or unique constraint on the pair

Question 6

Which ACID property means that a transaction’s changes survive an appropriate system failure after commit?

A. Atomicity

B. Consistency

C. Isolation

D. Durability

Correct Answer: D. Durability

Question 7

What is the main purpose of a repository layer?

A. To expose database credentials to clients

B. To encapsulate persistence operations and data-access details

C. To replace the database schema

D. To eliminate the need for testing

Correct Answer: B. To encapsulate persistence operations and data-access details

Question 8

Which statement best describes a migration?

A. A versioned, executable change to database structure or controlled data

B. A browser-based user session

C. A temporary connection borrowed from a pool

D. A type of SQL injection

Correct Answer: A. A versioned, executable change to database structure or controlled data

Question 9

What is the N+1 query problem?

A. One query is issued for a list and an additional query for each item

B. A database has exactly eleven tables

C. A transaction has eleven commits

D. A pool contains one connection too many

Correct Answer: A. One query is issued for a list and an additional query for each item

Question 10

Which is a limitation of ORM use?

A. ORMs can never represent relationships

B. ORMs always prevent SQL injection without configuration

C. Generated queries may be inefficient or obscure database-specific behavior

D. ORMs cannot perform create, read, update, or delete operations

Correct Answer: C. Generated queries may be inefficient or obscure database-specific behavior

Database Management
Chapter 9 of 14