DEV Community

Cover image for How Does a Backend Actually Connect to a Database?
Tanu Priya
Tanu Priya

Posted on

How Does a Backend Actually Connect to a Database?

Every time your API reads or writes data, there is a chain of components working behind the scenes: API → Backend → ORM/Database Driver → Connection Pool → Database → Result. The database driver handles communication, connection pools prevent your application from creating a new connection for every request, and ORMs make database operations easier without eliminating SQL underneath. Understanding this flow is essential once you move beyond building simple APIs.

You write:

const user = await prisma.user.findUnique({
  where: { id: 42 }
});
Enter fullscreen mode Exit fullscreen mode

And somehow a database returns the correct user.

It looks like one line of code.

It isn't.

Behind that line, your application has to communicate with another system, obtain a database connection, send a query, wait for the database to execute it, receive the result, and return that connection so another request can use it.

And when your application goes from 10 users to 10,000 users, those details stop being implementation trivia.

They become architecture.

This is the part of backend development that most tutorials hide.

Let's look at what actually happens.


The Request That Started It All

Imagine you're building a blogging platform.

A user opens their profile.

The browser sends:

GET /api/users/42
Enter fullscreen mode Exit fullscreen mode

Your backend receives the request and needs to return:

{
  "id": 42,
  "name": "Alex",
  "email": "alex@example.com"
}
Enter fullscreen mode Exit fullscreen mode

The obvious mental model is:

API → Database → Result
Enter fullscreen mode Exit fullscreen mode

The real system is closer to:

Client
   ↓
API
   ↓
Backend
   ↓
ORM / Database Driver
   ↓
Connection Pool
   ↓
Database
   ↓
Query Result
   ↓
Backend
   ↓
API Response
Enter fullscreen mode Exit fullscreen mode

That middle section is where most of the interesting engineering happens.


1. The Database Driver: Your Backend's Translator

Your Node.js, Python, Java, or Go application doesn't automatically know how to communicate with PostgreSQL or MySQL.

It needs a database driver.

For example, a Node.js application can use the PostgreSQL pg driver.

Conceptually:

Application
     ↓
Database Driver
     ↓
PostgreSQL
Enter fullscreen mode Exit fullscreen mode

A simplified example:

import pg from "pg";

const { Pool } = pg;

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});
Enter fullscreen mode Exit fullscreen mode

The driver handles the database-specific communication.

Your application says:

"Run this query."

The driver takes care of communicating with the database.

This separation is useful because your application doesn't need to understand every low-level detail of the database protocol.


2. Then There Is the Query

Once your application can communicate with the database, it needs to tell the database what it wants.

For example:

SELECT id, name, email
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

The database receives that query and does the actual database work.

It has to determine:

  • Which table is involved?
  • Which rows match?
  • Should an index be used?
  • How should the query be executed?
  • What data needs to be returned?

The database then sends the result back to your application.

For example:

[
  {
    "id": 42,
    "name": "Alex",
    "email": "alex@example.com"
  }
]
Enter fullscreen mode Exit fullscreen mode

Your backend turns that into an API response.

So:

HTTP Request
     ↓
Backend
     ↓
SQL Query
     ↓
Database
     ↓
Result
     ↓
Backend
     ↓
HTTP Response
Enter fullscreen mode Exit fullscreen mode

That's the basic version.

But there's a scalability problem.


3. Why You Can't Just Open a New Connection for Every Request

Suppose your API receives 1,000 requests.

A naive approach would be:

Request 1 → Open Connection → Query → Close
Request 2 → Open Connection → Query → Close
Request 3 → Open Connection → Query → Close
...
Enter fullscreen mode Exit fullscreen mode

It sounds reasonable.

It isn't.

Creating database connections consumes resources. Doing it repeatedly adds unnecessary overhead and can put significant pressure on the database.

Instead, production applications generally reuse connections.

That's what connection pooling is for.


4. Connection Pools: The Unsung Hero of Backend Systems

A connection pool maintains a set of reusable database connections.

Instead of:

Request
   ↓
Create Connection
   ↓
Query
   ↓
Destroy Connection
Enter fullscreen mode Exit fullscreen mode

you get:

Request
   ↓
Borrow Connection
   ↓
Run Query
   ↓
Return Connection
Enter fullscreen mode Exit fullscreen mode

A pool might look like:

             Connection Pool

        ┌─────────────────────┐
        │ Connection 1        │
        │ Connection 2        │
        │ Connection 3        │
        │ Connection 4        │
        └─────────────────────┘
                  │
                  ↓
               Database
Enter fullscreen mode Exit fullscreen mode

Imagine four requests arrive.

They can use the available connections:

Request A → Connection 1
Request B → Connection 2
Request C → Connection 3
Request D → Connection 4
Enter fullscreen mode Exit fullscreen mode

When Request A finishes:

Connection 1 → Available Again
Enter fullscreen mode Exit fullscreen mode

Another request can reuse it.

The connection doesn't need to be recreated.


5. More Connections Doesn't Mean More Speed

This is one of those backend lessons that becomes important only after you start dealing with production systems.

You might think:

"If 10 connections are good, 100 connections must be better."

Not necessarily.

Your database has finite resources.

Too many connections can create:

  • memory pressure
  • CPU overhead
  • contention
  • increased latency
  • unnecessary database load

So a connection pool isn't just a performance trick.

It's also a concurrency control mechanism.

For example:

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10
});
Enter fullscreen mode Exit fullscreen mode

The correct pool size depends on your workload, database, server resources, and deployment architecture.

The important lesson is:

Your backend shouldn't be allowed to create an unlimited number of database connections.


6. Where Does an ORM Fit?

Now let's make the architecture more realistic.

Most modern applications don't write SQL for every simple database operation.

You might use an ORM such as Prisma.

Instead of:

SELECT *
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

you write:

const user = await prisma.user.findUnique({
  where: {
    id: 42
  }
});
Enter fullscreen mode Exit fullscreen mode

Much cleaner.

But don't let the abstraction fool you.

The database hasn't disappeared.

The architecture is now:

Your Application
       ↓
      ORM
       ↓
Database Driver
       ↓
Connection Pool
       ↓
Database
Enter fullscreen mode Exit fullscreen mode

The ORM gives you a more convenient interface for working with the database.


7. ORM Doesn't Mean "No SQL"

This is probably the most important misconception to clear up.

If you use Prisma, Sequelize, TypeORM, or another ORM, you're not avoiding databases.

You're avoiding manually writing every database query.

The underlying system still needs database operations.

So when you write:

prisma.user.findUnique({
  where: { id: 42 }
});
Enter fullscreen mode Exit fullscreen mode

the database still has to:

Receive Query
     ↓
Understand Query
     ↓
Create Execution Plan
     ↓
Read Data
     ↓
Return Result
Enter fullscreen mode Exit fullscreen mode

The ORM simply provides a higher-level way of expressing what you want.

That's why SQL knowledge doesn't become irrelevant when you learn an ORM.

If anything, it becomes more useful when things stop working as expected.


8. Raw SQL vs ORM: The Tradeoff

This isn't really an argument about which technology is "better."

It's about how much control versus abstraction you need.

Raw SQL

SELECT
    users.name,
    COUNT(posts.id) AS post_count
FROM users
LEFT JOIN posts
    ON users.id = posts.author_id
GROUP BY users.id;
Enter fullscreen mode Exit fullscreen mode

You control the query directly.

That's valuable for:

  • complex joins
  • aggregations
  • database-specific features
  • performance tuning
  • advanced queries

But you're also responsible for writing and maintaining the SQL.

ORM

const users = await prisma.user.findMany({
  include: {
    posts: true
  }
});
Enter fullscreen mode Exit fullscreen mode

This can make everyday development much faster.

You may also get:

  • type safety
  • migrations
  • schema modeling
  • autocomplete
  • relationship handling
  • developer tooling

The tradeoff is abstraction.

The ORM decides how your high-level operation gets translated into database operations.


9. The Approach I'd Actually Recommend

For most application development:

Use the ORM where it makes the code simpler.

But don't treat it as a black box.

If you encounter:

  • a slow query
  • unexpected database load
  • a complicated join
  • an inefficient query
  • a database-specific requirement

drop down a level.

Think of it like this:

                 Your Application
                        │
                        ↓
              ┌──────────────────┐
              │       ORM        │
              └──────────────────┘
                    │        │
             Simple Queries  │
                    │        ↓
                    │     Raw SQL
                    │        │
                    └────┬───┘
                         ↓
                  Database Driver
                         ↓
                  Connection Pool
                         ↓
                      Database
Enter fullscreen mode Exit fullscreen mode

The goal isn't to eliminate SQL.

The goal is to use the right abstraction for the job.


10. What Happens When the Database Is Slow?

Here's where understanding the architecture starts paying off.

Your endpoint suddenly takes three seconds.

You look at the backend code:

const user = await prisma.user.findUnique({
  where: { id: 42 }
});
Enter fullscreen mode Exit fullscreen mode

Nothing obviously looks wrong.

So where is the bottleneck?

It could be:

API
 ↓
Backend
 ↓
ORM
 ↓
Connection Pool
 ↓
Database
Enter fullscreen mode Exit fullscreen mode

Maybe:

  • the connection pool is exhausted
  • the query is slow
  • an index is missing
  • the database is overloaded
  • too much data is being returned
  • another query is consuming resources

This is why knowing only the ORM isn't enough.

You need to understand the layers underneath it.


11. Indexes: When the Database Has Too Much Data to Search

Imagine your users table contains 10 million rows.

You run:

SELECT *
FROM users
WHERE email = 'alex@example.com';
Enter fullscreen mode Exit fullscreen mode

Without an appropriate index, the database may need to inspect a large amount of data to find the matching record.

With an index, the database can often locate the relevant record much more efficiently.

Conceptually:

Without useful index:

Database
 ↓
Check many rows
 ↓
Find matching row


With index:

Database
 ↓
Index
 ↓
Matching row
Enter fullscreen mode Exit fullscreen mode

This is why database performance isn't simply about writing faster backend code.

Sometimes the bottleneck is inside the database itself.


12. What About Multiple Operations?

Suppose a user publishes a post.

Your application needs to:

Create Post
    ↓
Update User Statistics
    ↓
Create Activity Record
Enter fullscreen mode Exit fullscreen mode

What happens if:

Create Post          ✅
Update Statistics    ❌
Enter fullscreen mode Exit fullscreen mode

Now your data may be inconsistent.

This is where transactions become important.

Conceptually:

BEGIN
  ↓
Create Post
  ↓
Update Statistics
  ↓
Create Activity
  ↓
COMMIT
Enter fullscreen mode Exit fullscreen mode

If something fails:

BEGIN
  ↓
Create Post
  ↓
Update Statistics
  ↓
ERROR
  ↓
ROLLBACK
Enter fullscreen mode Exit fullscreen mode

Instead of treating each operation independently, the database can treat the group as one logical unit.

This is another reason understanding databases matters even if an ORM handles the syntax for you.


13. Things Get More Interesting When You Scale

Imagine your application grows.

You start with:

1 Backend Server
       ↓
   Database
Enter fullscreen mode Exit fullscreen mode

Then traffic increases.

You add more servers:

                 Load Balancer
                /      |      \
               ↓       ↓       ↓
            Server  Server  Server
               \       |       /
                \      |      /
                 Database
Enter fullscreen mode Exit fullscreen mode

Now there's something easy to overlook:

Each application server may have its own connection pool.

Suppose you configure:

20 connections per server
Enter fullscreen mode Exit fullscreen mode

and deploy:

5 backend servers
Enter fullscreen mode Exit fullscreen mode

You could potentially have:

5 × 20 = 100
Enter fullscreen mode Exit fullscreen mode

database connections.

So scaling your application servers can also increase the number of connections reaching your database.

This is why database configuration cannot be considered separately from application architecture.


14. The Production Stack Is More Than "Backend + Database"

A beginner's architecture often looks like:

Frontend
   ↓
Backend
   ↓
Database
Enter fullscreen mode Exit fullscreen mode

A production system can look more like:

                    Client
                      ↓
                Load Balancer
                      ↓
              Backend Servers
                      ↓
                ORM / Driver
                      ↓
               Connection Pool
                      ↓
          ┌───────────┴───────────┐
          ↓                       ↓
       Cache                  Database
                                  ↓
                               Indexes
                                  ↓
                              Storage
Enter fullscreen mode Exit fullscreen mode

And depending on the application, you may also introduce:

  • queues
  • background workers
  • read replicas
  • monitoring
  • caching layers
  • database migrations
  • transaction management

The database is not an isolated component.

It's part of a larger system.


15. Common Mistakes Developers Make

Creating a connection for every request

This defeats one of the main benefits of connection pooling.

Setting the pool size arbitrarily high

More connections can eventually make the database less stable, not more performant.

Assuming the ORM automatically creates efficient queries

ORMs are useful abstractions, but developers still need to understand the queries their applications generate.

Ignoring indexes

A perfectly written API can still be slow because the database is doing unnecessary work.

Never learning SQL

This becomes painful when you need to debug a slow query or understand why the database isn't behaving as expected.

Treating the database as a simple storage box

A database isn't just where your JSON happens to live.

It is an engine that manages querying, indexing, concurrency, consistency, transactions, and much more.


The Mental Model That Actually Matters

If you remember only one diagram from this article, make it this one:

                    API Request
                         ↓
                  Backend Server
                         ↓
                   ORM / Driver
                         ↓
                  Connection Pool
                         ↓
                     Database
                         ↓
                   Query Result
                         ↓
                  Backend Server
                         ↓
                  API Response
Enter fullscreen mode Exit fullscreen mode

Each layer solves a different problem.

Layer Responsibility
API Communicates with clients
Backend Handles application logic
ORM Provides a high-level database interface
Driver Communicates with the database
Connection Pool Reuses and manages connections
Database Stores and processes data

Once you understand this pipeline, a lot of backend concepts stop feeling disconnected.

Indexes make database lookups more efficient.

Transactions protect consistency.

Connection pools manage database concurrency.

ORMs provide abstraction.

SQL gives you direct control.

Caching reduces unnecessary database work.

They're all pieces of the same system.


Final Take

The interesting thing about database communication is that the code you write is often the smallest part of the story.

You might write:

const user = await prisma.user.findUnique({
  where: { id: 42 }
});
Enter fullscreen mode Exit fullscreen mode

But underneath that line is an entire pipeline:

Application
    ↓
ORM
    ↓
Database Driver
    ↓
Connection Pool
    ↓
Database
    ↓
Query Execution
    ↓
Result
Enter fullscreen mode Exit fullscreen mode

When an application is small, you can get away with treating that pipeline as a black box.

When the application grows, you can't.

A slow API might actually be a slow query.

A database outage might actually be an exhausted connection pool.

A scaling problem might actually be too many connections across multiple servers.

And a seemingly harmless ORM operation might generate far more database work than you expected.

That's why learning backend development isn't just about learning frameworks.

It's about understanding what those frameworks are doing underneath the surface.

The best backend developers don't just know how to query a database. They understand what happens between the API request and the row coming back.

And once you understand that journey, the next question naturally follows:

How does the database itself find, organize, and protect all those rows?

That's where tables, relationships, primary keys, foreign keys, indexes, normalization, transactions, and database design begin.

Top comments (0)