PostgreSQL and Applications: Connecting Your Database to Real Projects

Welcome back.
In the previous lesson, you built a small shop database.
You created:
customers
categories
products
orders
order_items
You used:
- primary keys;
- foreign keys;
JOIN;- aggregate functions;
- indexes;
- real project thinking.
Very good.
Now PostgreSQL is no longer just a mysterious terminal creature.
It is becoming a real tool.
But there is still one very important question:
How does an application actually connect to PostgreSQL?
Because in real projects, users do not open psql and type SQL manually.
Usually.
A backend application connects to the database.
Then the application reads data.
Writes data.
Updates data.
Deletes data.
Hopefully not all data.
That would be a dramatic Tuesday.
Today we will learn how applications connect to PostgreSQL.
This lesson is not about one specific framework.
The ideas apply to:
Django
Spring Boot
Node.js
Express
NestJS
Laravel
Rails
Go applications
any backend that needs PostgreSQL
Frameworks change.
Connection principles stay.
Very useful.
Very database.
Very “please do not put your password directly in the code”.
What You Will Learn
In this lesson, you will learn:
- how applications connect to PostgreSQL;
- what a database connection is;
- what a host, port, database name, username, and password mean;
- what a connection string is;
- how to create a database user for an application;
- why you should not use the
postgressuperuser in your app; - how to use environment variables;
- how a
.envfile works; - common connection examples;
- basic security rules;
- common connection errors;
- how local development differs from production.
By the end of this lesson, you will understand the bridge between PostgreSQL and backend applications.
Because a database without an application is useful.
But a database connected to an application becomes powerful.
Like a warehouse connected to a shop.
Or like a coffee machine connected to a tired developer.
Important infrastructure.
The Basic Idea
An application needs connection information to talk to PostgreSQL.
Usually it needs:
host
port
database name
username
password
Example:
host: localhost
port: 5432
database: shop_db
username: shop_user
password: strong_password
The application uses these values to open a connection.
Then it can send SQL queries.
For example:
SELECT * FROM products;
The application sends the query.
PostgreSQL runs it.
PostgreSQL returns the result.
The application uses the result.
Maybe it shows products on a website.
Maybe it creates an API response.
Maybe it generates a report.
Maybe it breaks because someone forgot a semicolon.
Classic.
PostgreSQL Host
The host tells the application where PostgreSQL is running.
For local development, the host is often:
localhost
or:
127.0.0.1
This means:
PostgreSQL is running on the same machine as the application.
Example:
host=localhost
In production, PostgreSQL may run on another server.
Example:
host=db.example.com
or:
host=10.0.0.5
or inside Docker:
host=postgres
The host depends on where the database lives.
The application needs the correct address.
Otherwise it will knock on the wrong door.
PostgreSQL will not answer from a house where it does not live.
Very reasonable.
PostgreSQL Port
PostgreSQL usually listens on port:
5432
So a common connection uses:
port=5432
A port is like a door number on a server.
The server may run many services.
PostgreSQL uses port 5432 by default.
If you changed the port, you must use the new one.
But for beginners, most of the time:
5432
is the answer.
A rare moment of simplicity.
Enjoy it.
Database Name
A PostgreSQL server can contain many databases.
For example:
learning_postgresql
shop_db
portfolio_db
blog_db
Your application must know which database to use.
Example:
database=shop_db
If your application connects to the wrong database, strange things happen.
Tables may be missing.
Data may look old.
Your brain may start debugging the wrong universe.
Always check the database name.
Small typo.
Big headache.
Username and Password
PostgreSQL uses users and passwords to control access.
Example:
username=shop_user
password=strong_password
The user determines what the application can do.
A user can have permissions like:
connect to a database
read tables
insert rows
update rows
delete rows
create tables
Do not give every application full superuser power.
That is like giving a child a chainsaw because they wanted to cut paper.
Technically effective.
Deeply unwise.
Do Not Use the postgres User in Applications
PostgreSQL often has a default administrative user called:
postgres
This user is powerful.
Very powerful.
Too powerful for normal applications.
Do not use postgres as your application user.
Instead, create a specific user for your application.
Example:
shop_user
blog_user
app_user
portfolio_user
Why?
Because if your application has a bug or gets attacked, you want to limit the damage.
An application user should have only the permissions it needs.
Not the keys to the whole kingdom.
The database kingdom has dragons.
And invoices.
Create a Database for an Application
Open PostgreSQL as the postgres user:
sudo -iu postgres psql
Create a database:
CREATE DATABASE shop_db;
Create a user:
CREATE USER shop_user WITH PASSWORD 'change_this_password';
Give the user access to the database:
GRANT CONNECT ON DATABASE shop_db TO shop_user;
Now connect to the database:
\c shop_db
Give permissions on the public schema:
GRANT USAGE ON SCHEMA public TO shop_user;
Allow the user to work with tables:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO shop_user;
Allow the user to use sequences.
This is important for SERIAL IDs.
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO shop_user;
For future tables, you can set default privileges:
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO shop_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO shop_user;
This is already much better than using the postgres superuser.
The application gets access.
But not unlimited power.
Very civilized.
Test the New User
Exit PostgreSQL:
\q
Now try connecting as the new user:
psql -h localhost -U shop_user -d shop_db
PostgreSQL will ask for the password.
Enter the password you created.
If connection works, you will enter psql.
Then test:
SELECT current_user;
You should see:
shop_user
Good.
Now your application can use this user too.
The database door opens.
But only with the correct key.
Very secure.
Very adult.
What Is a Connection String?
A connection string puts all connection information in one line.
Common PostgreSQL format:
postgresql://username:password@host:port/database
Example:
postgresql://shop_user:change_this_password@localhost:5432/shop_db
This contains:
username: shop_user
password: change_this_password
host: localhost
port: 5432
database: shop_db
Many tools and frameworks use connection strings.
They are compact.
Useful.
And dangerous if you paste them everywhere.
Because they contain passwords.
Treat connection strings like keys.
Do not publish them.
Do not commit them to GitHub.
Do not send them to random people.
Do not put them in screenshots.
Your future self will thank you.
Security people will also stop screaming.
Environment Variables
Applications usually read database settings from environment variables.
Environment variables are values stored outside the code.
Example:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=shop_db
DB_USER=shop_user
DB_PASSWORD=change_this_password
Or:
DATABASE_URL=postgresql://shop_user:change_this_password@localhost:5432/shop_db
Why use environment variables?
Because code should not contain secrets.
Bad:
password directly inside source code
Good:
password in environment variable
This lets you use different settings for:
local development
testing
production
Same code.
Different configuration.
Very useful.
Very professional.
Very “I have learned from painful mistakes”.
The .env File
During local development, many projects use a .env file.
Example:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=shop_db
DB_USER=shop_user
DB_PASSWORD=change_this_password
Or:
DATABASE_URL=postgresql://shop_user:change_this_password@localhost:5432/shop_db
The application reads this file when it starts.
Important:
Do not commit .env files with real passwords.
Add .env to .gitignore.
Example .gitignore:
.env
.env.local
.env.production
You can create a safe example file:
.env.example
Example:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=shop_db
DB_USER=shop_user
DB_PASSWORD=your_password_here
This shows other developers what variables are needed.
But it does not expose real secrets.
Good practice.
Very good.
Gold star.
Database gold star.
Example: Generic Application Settings
Many applications need settings like this:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=shop_db
DB_USER=shop_user
DB_PASSWORD=change_this_password
Then the application builds a connection.
Conceptually:
Connect to DB_HOST on DB_PORT.
Use DB_NAME.
Login with DB_USER and DB_PASSWORD.
Some frameworks prefer one URL:
DATABASE_URL=postgresql://shop_user:change_this_password@localhost:5432/shop_db
Both styles are common.
Which one you use depends on the framework.
But the idea is the same.
Your app needs directions to the database.
Without directions, it wanders around like a lost tourist with a broken GPS.
Example: Django Settings
In Django, database settings often look like this:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "shop_db",
"USER": "shop_user",
"PASSWORD": "change_this_password",
"HOST": "localhost",
"PORT": "5432",
}
}
But putting the password directly here is not good.
Better idea:
import os
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME"),
"USER": os.environ.get("DB_USER"),
"PASSWORD": os.environ.get("DB_PASSWORD"),
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
}
}
Now the password comes from environment variables.
Much better.
Django gets the connection details.
Your code stays safer.
Everyone is less nervous.
Except maybe the CSS developer.
But that is a different course.
Example: Spring Boot Settings
In Spring Boot, application.properties can look like this:
spring.datasource.url=jdbc:postgresql://localhost:5432/shop_db
spring.datasource.username=shop_user
spring.datasource.password=change_this_password
Better with environment variables:
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
Then your environment can contain:
DB_URL=jdbc:postgresql://localhost:5432/shop_db
DB_USER=shop_user
DB_PASSWORD=change_this_password
This is much safer than hardcoding secrets.
In real projects, the database password should not live in your Git repository.
Git remembers everything.
Like an elephant.
But with commit history.
Example: Node.js Connection
A Node.js application may use a connection string:
DATABASE_URL=postgresql://shop_user:change_this_password@localhost:5432/shop_db
Then the app reads it.
Conceptually:
const connectionString = process.env.DATABASE_URL;
Many Node.js libraries support this style.
The important idea is not the exact library.
The important idea is:
Read secrets from environment variables.
Do not hardcode them.
This rule follows you across languages.
Python.
Java.
JavaScript.
Go.
PHP.
Everywhere.
Secrets in code are like open windows in winter.
Bad idea.
Expensive later.
Local Development vs Production
Local development usually means:
application and database run on your computer
host = localhost
port = 5432
Production usually means:
application runs on a server
database may run on another server
host is not localhost
password is stronger
security matters more
backups matter more
monitoring matters more
In local development, mistakes are annoying.
In production, mistakes become invoices.
Very motivational.
That is why configuration should be flexible.
You may have:
local database
test database
production database
Each environment has different values.
Same application code.
Different environment variables.
This is normal.
This is professional.
This is how we avoid changing code just to connect somewhere else.
Docker and PostgreSQL Host Names
If your application and PostgreSQL run inside Docker Compose, the host is often the service name.
Example:
services:
app:
environment:
DB_HOST: postgres
DB_PORT: 5432
postgres:
image: postgres
Inside Docker, the app may connect to:
postgres
not:
localhost
Why?
Because inside the app container, localhost means the app container itself.
Not the PostgreSQL container.
This is a very common mistake.
Very common.
Very painful.
Very Docker.
Simple rule:
From your computer: localhost may work.
From another container: use the service name.
So in Docker Compose, the database host might be:
postgres
or:
db
depending on your service name.
Docker networking is powerful.
And occasionally spicy.
Common Connection Errors
Connection Refused
Example error:
connection refused
This usually means:
PostgreSQL is not running.
Wrong host.
Wrong port.
PostgreSQL is not listening there.
Firewall blocks connection.
Check PostgreSQL status:
systemctl status postgresql
On some systems, the service name may differ.
You can also try:
psql -h localhost -U shop_user -d shop_db
If psql cannot connect, your application probably cannot connect either.
Do not blame the framework first.
First check the database.
Frameworks are guilty often.
But not always.
Password Authentication Failed
Example:
password authentication failed for user
This usually means:
wrong username
wrong password
user does not exist
environment variable has wrong value
Check the username:
SELECT current_user;
Check that the user exists:
\du
If needed, reset the password:
ALTER USER shop_user WITH PASSWORD 'new_password_here';
Then update your environment variable.
Do not update only your memory.
Applications cannot read your memory.
Thankfully.
Database Does Not Exist
Example:
database "shop_db" does not exist
This means exactly what it says.
Usually.
Check databases:
\l
Create the database if needed:
CREATE DATABASE shop_db;
Or fix the database name in your configuration.
This error is often just a typo.
A tiny typo with a big attitude.
Permission Denied
Example:
permission denied for table products
This means the user connected successfully.
But it does not have permission to do something.
Give permissions:
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO shop_user;
For sequences:
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO shop_user;
If the app can read but cannot insert rows with SERIAL IDs, sequence permissions may be the problem.
PostgreSQL permissions are powerful.
Also slightly annoying.
Like a serious security guard with paperwork.
Table Does Not Exist
Example:
relation "products" does not exist
Possible reasons:
You connected to the wrong database.
The table was not created.
The table is in another schema.
The migration did not run.
The table name is different.
Check tables:
\dt
Check current database:
SELECT current_database();
This error often happens when your application connects to one database, but you created tables in another.
Classic.
Painful.
Very educational.
Basic Security Rules
Here are simple rules.
Not advanced security.
Basic survival.
Do Not Hardcode Passwords
Bad:
password inside source code
Good:
password in environment variable
Code can be shared.
Secrets should not be shared.
Very simple.
Very ignored.
Very dangerous.
Do Not Commit .env
Add .env to .gitignore.
.env
.env.local
.env.production
If you accidentally commit a real password, change it.
Do not just delete the file from the latest commit and pretend nothing happened.
Git history remembers.
Git is not your friend in this situation.
Git is a witness.
Use a Separate Database User
Do not use the postgres superuser in your application.
Create an app-specific user:
shop_user
Give only needed permissions.
This limits damage if something goes wrong.
And something eventually goes wrong.
That is not pessimism.
That is software development.
Use Strong Passwords
Do not use:
password
123456
postgres
admin
qwerty
These are not passwords.
These are invitations.
Use strong passwords.
Especially in production.
Your database deserves better.
Keep Production Database Private
A production PostgreSQL database should usually not be open to the whole internet.
Better:
private network
firewall rules
limited IP access
secure hosting configuration
If the database is publicly reachable, security becomes much more serious.
Databases like privacy.
So do users.
So do lawyers.
Application Migrations
Many frameworks use migrations.
A migration is a controlled database change.
Examples:
create table
add column
change column type
create index
drop table
Instead of manually changing the database every time, the framework tracks changes.
Django has migrations.
Spring Boot often uses tools like Flyway or Liquibase.
Node.js ORMs also have migration tools.
The idea is:
Database structure should be versioned.
This matters because applications and databases must match.
If your code expects a column called email, but the database does not have it, the app breaks.
Very confidently.
Migrations help keep structure under control.
They are not magic.
But they are better than random manual changes at midnight.
ORM vs Raw SQL
Many applications use an ORM.
ORM means Object-Relational Mapping.
Examples:
Django ORM
Hibernate / JPA
Prisma
TypeORM
Sequelize
SQLAlchemy
An ORM lets you work with database rows as objects.
Example idea:
Product object
Customer object
Order object
The ORM generates SQL.
This is convenient.
But you still need to understand SQL.
Why?
Because when something becomes slow or broken, the ORM will not save your soul.
It will generate SQL.
PostgreSQL will execute SQL.
Performance problems are still SQL problems.
So learning SQL is not wasted.
Even if you use an ORM.
Actually, especially if you use an ORM.
Because now you know what is happening under the blanket.
And sometimes under the blanket there is a monster query.
Common Application Flow
A typical backend flow looks like this:
User opens website.
Frontend sends request to backend.
Backend connects to PostgreSQL.
Backend runs query.
PostgreSQL returns data.
Backend returns JSON or HTML.
Frontend shows result.
Example:
GET /products
Backend query:
SELECT
p.id,
p.name,
p.price,
c.name AS category_name
FROM products AS p
JOIN categories AS c
ON p.category_id = c.id
ORDER BY p.name;
Backend returns JSON:
[
{
"id": 1,
"name": "Laptop",
"price": 900.00,
"category": "Electronics"
}
]
This is how PostgreSQL becomes part of an application.
The user never sees SQL.
But SQL does the work.
Like a backstage worker.
Quiet.
Important.
Slightly underappreciated.
Connection Pooling
Opening a database connection takes time.
If every request opens a new connection and closes it immediately, performance can suffer.
Many applications use a connection pool.
A connection pool keeps several database connections open and reuses them.
Simple idea:
Do not create a new connection every time.
Reuse existing connections.
Most frameworks handle this for you.
But it is good to know the idea.
Because production systems care about connections.
Too many connections can overload PostgreSQL.
Too few connections can slow the app.
Balance matters.
Like coffee.
Too little is bad.
Too much and you can hear colors.
Practice
Create a new database:
CREATE DATABASE app_demo_db;
Create a new user:
CREATE USER app_demo_user WITH PASSWORD 'change_this_password';
Grant connection:
GRANT CONNECT ON DATABASE app_demo_db TO app_demo_user;
Connect to the database:
\c app_demo_db
Create a simple table:
CREATE TABLE messages (
id SERIAL PRIMARY KEY,
title VARCHAR(150) NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Grant table permissions:
GRANT USAGE ON SCHEMA public TO app_demo_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_demo_user;
Grant sequence permissions:
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_demo_user;
Insert a test row:
INSERT INTO messages (title, content)
VALUES ('Hello App', 'This row could be read by a backend application.');
Test connection:
psql -h localhost -U app_demo_user -d app_demo_db
Then run:
SELECT * FROM messages;
If this works, you have a database, a user, permissions, and a working connection.
That is the foundation of application integration.
Not flashy.
Very important.
Like good wiring.
Nobody sees it.
Everyone complains when it fails.
Mini Challenge
Create a database for a blog application.
Requirements:
database name: blog_app_db
user name: blog_app_user
tables: authors, posts
Rules:
- each author has a unique email;
- each post belongs to an author;
- post title is required;
- post content can be long text;
- posts have a creation date.
Create the database:
CREATE DATABASE blog_app_db;
Create the user:
CREATE USER blog_app_user WITH PASSWORD 'change_this_password';
Grant connection:
GRANT CONNECT ON DATABASE blog_app_db TO blog_app_user;
Connect:
\c blog_app_db
Create tables:
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
author_id INTEGER NOT NULL REFERENCES authors(id),
title VARCHAR(150) NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Grant permissions:
GRANT USAGE ON SCHEMA public TO blog_app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO blog_app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO blog_app_user;
Create useful indexes:
CREATE INDEX idx_posts_author_id
ON posts(author_id);
CREATE INDEX idx_posts_created_at
ON posts(created_at);
Now create a .env.example file for the application:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=blog_app_db
DB_USER=blog_app_user
DB_PASSWORD=your_password_here
Or:
DATABASE_URL=postgresql://blog_app_user:your_password_here@localhost:5432/blog_app_db
Then answer:
Which values are safe to commit?
Which values should stay secret?
Why should .env be ignored by Git?
Why should the app not use the postgres user?
This is practical.
This is real.
This is the boring foundation that prevents exciting disasters.
And in databases, boring is often good.
Very good.
Common Mistakes
Using postgres User Everywhere
Do not do this.
The postgres user is for administration.
Your app should have its own user.
Give it only the permissions it needs.
This is basic security.
Basic security is not optional.
It is like wearing shoes in a workshop.
Hardcoding Secrets
Bad:
spring.datasource.password=my_real_password
Bad:
"PASSWORD": "my_real_password"
Bad:
const password = "my_real_password";
Use environment variables.
Your code is not a vault.
Forgetting Sequence Permissions
If your app can insert rows but fails with sequence permission errors, remember:
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
SERIAL uses sequences.
Sequences need permissions.
PostgreSQL is precise.
Sometimes painfully precise.
Connecting to the Wrong Database
Always check:
SELECT current_database();
Many bugs are just:
I created the table in one database.
My app connects to another database.
This is painful.
But very common.
Using localhost Incorrectly in Docker
Inside Docker Compose, localhost from the app container usually means the app container.
Not the database container.
Use the database service name.
Example:
postgres
or:
db
Docker is not wrong.
It is just very literal.
Like PostgreSQL.
They probably get along.
Summary
Today you learned:
- applications connect to PostgreSQL using host, port, database name, username, and password;
- PostgreSQL usually uses port
5432; - a connection string combines connection details in one URL;
- application secrets should come from environment variables;
.envfiles are useful locally but should not be committed with real secrets;- applications should not use the
postgressuperuser; - app-specific database users are safer;
- users need permissions on tables and sequences;
psqlis useful for testing connections;- Docker changes how host names work;
- common errors include wrong password, wrong database, missing permissions, and connection refused;
- migrations help manage database structure;
- ORMs are useful, but SQL knowledge is still important;
- connection pooling helps applications reuse database connections.
This is an important lesson.
You now understand how PostgreSQL connects to real applications.
This is where database knowledge becomes backend power.
The database stores the truth.
The application uses the truth.
The user sees the result.
And somewhere in the middle, a developer hopes the environment variables are correct.
Very real.
Very professional.
Very Monday.
Next Lesson
In the next lesson, we will talk about backups, restore, and basic database maintenance.
Because creating a database is good.
Using a database is better.
But being able to recover your data after something goes wrong is priceless.
Backups are boring.
Until the day they save your life.
Then they become beautiful.