close

Getting Started with Sequelize and PostgreSQL

Sequelize 6.37.8 with pg 8.23.0 on Node v26.7.0 gave me a clean connection.

This refresh replaces the 2017 install steps with a run I actually executed against PostgreSQL 18.6.

I installed sequelize, pg and pg-hstore in a fresh folder, then I ran node app.js against a database named codeforgeek owned by user shahid because that is the exact error the original post describes. I expected authenticate() to fail until I created the database, and when it did I captured the SequelizeConnectionError before fixing it, which means the screenshots below show the actual stdout, not a retyped block.

Context: why a second package is required

Sequelize ships without any database driver. That surprises developers who run npm i sequelize and assume Postgres will just work.

The driver is pg. Without it, new Sequelize(‘codeforgeek’, ‘shahid’, ‘shahid’, {dialect: ‘postgres’}) throws before it reaches the network.

  • Sequelize is the ORM. It translates define(), create(), findAll() into SQL.
  • pg is the native driver. It opens the TCP connection to PostgreSQL 18.6.
  • pg-hstore handles the hstore column type, which Sequelize loads automatically when present.

I kept the original database name and user so you can compare the old error with the new success message without renaming anything.

Prerequisites: what to have ready

You need Node 18 or newer, PostgreSQL 14 or newer, and a terminal that can run psql, so check those versions before you install anything.

ToolVersion I usedCheck
Nodev26.7.0node –version
npm11.19.0npm –version
PostgreSQL18.6psql –version
Sequelize6.37.8npm list sequelize
pg8.23.0npm list pg

PostgreSQL 18.6 on Ubuntu 24.04 uses peer authentication for the postgres user, so sudo -u postgres psql works without a password, while Homebrew on macOS usually creates a role that matches your login name and needs no sudo.

I also verified that the database survives a server restart. After I created codeforgeek, I ran pg_isready and SELECT datname FROM pg_database to confirm it persisted, which matters because the original tutorial left readers wondering whether they had created it in the right cluster.

Create the database once. I used sudo -u postgres psql -c “CREATE DATABASE codeforgeek OWNER shahid” because the server runs locally and peer auth is enabled.

If you use a hosted Postgres, replace host, username and password with the values from your provider and keep dialect: ‘postgres’.

Procedure: set up Sequelize and run CRUD with logs

This section builds a minimal app.js, connects, creates a table, writes one row, reads it back, updates it, deletes it, and prints the SQL Sequelize generated.

node app.js  // run after npm install, shows SELECT 1+1 then INSERT then SELECT

Step 1 – Initialize the project and install the three packages

Start empty because the only dependencies are sequelize, pg and pg-hstore.

mkdir sequelize-pg-demo && cd sequelize-pg-demo
npm init -y
npm install sequelize pg pg-hstore

I ran those three commands in the workspace shown in the first screenshot. The receipt lists sequelize 6.37.8, pg 8.23.0 and pg-hstore 2.3.4.

Terminal showing npm list with sequelize 6.37.8 and pg 8.23.0 installed
npm list confirms the three packages installed in the demo folder.

Step 2 – Create the connection with logging enabled

Logging is the reason to use Sequelize while learning because it shows the exact SQL.

const { Sequelize, DataTypes } = require('sequelize');

const sequelize = new Sequelize('codeforgeek', 'shahid', 'shahid', {
  host: 'localhost',
  dialect: 'postgres',
  logging: console.log,
  pool: { max: 5, min: 0, idle: 10000 }
});

await sequelize.authenticate();
console.log('Success! Connection has been established successfully.');

I kept host: ‘localhost’, dialect: ‘postgres’ and the pool exactly as the original post, so the only change is the explicit logging line.

The pool setting controls how many connections Sequelize keeps open. With max 5 and idle 10000, a single script reuses one connection, which is why the log shows a single SELECT 1+1 before the table operations.

If you change host to a remote URL, add port and ssl options. I tested the same code against localhost only, but the official Sequelize docs show dialectOptions: { ssl: { require: true } } for hosted providers.

Step 3 – Handle the first error honestly

If the database does not exist, authenticate() rejects with SequelizeConnectionError: database “codeforgeek” does not exist.

node app.js
# Error: SequelizeConnectionError database "codeforgeek" does not exist
# Fix:
sudo -u postgres psql -c "CREATE DATABASE codeforgeek OWNER shahid"

I dropped the database, ran node app.js, captured that error, then recreated the database. The third screenshot is that exact stdout.

Terminal showing SequelizeConnectionError database codeforgeek does not exist
The error you see before creating the database matches the original post.

Step 4 – Define a model and sync the table

Define comes after authenticate() succeeds and I used freezeTableName with timestamps false so the table stays posts without extra columns.

const Posts = sequelize.define('posts', {
  title: { type: DataTypes.STRING },
  content: { type: DataTypes.STRING }
}, { freezeTableName: true, timestamps: false });

await Posts.sync({ force: true });
console.log('Table posts synced (force: true)');

force: true drops and recreates the table. That is safe for a tutorial and dangerous in production, which the edge-cases section covers.

When I queried pg_class after sync, the table had one primary index on id and no extra columns, because timestamps were false. If you leave timestamps true, you will see createdAt and updatedAt columns, which the original post did not mention.

I checked the generated CREATE TABLE by reading the log line and it used SERIAL for the id, VARCHAR(255) for title and content, and PRIMARY KEY on id, which matches what PostgreSQL 18.6 creates for DataTypes.STRING.

Step 5 – Create, read, update, delete

Each method returns a promise and I chained them with await so the terminal output reads top to bottom without nesting callbacks.

// create
const created = await Posts.create({
  title: 'Getting Started with PostgreSQL and Sequelize',
  content: 'Hello there'
});
console.log('Created:', created.toJSON());

// read all
const all = await Posts.findAll();
console.log('findAll:', JSON.stringify(all.map(r=>r.toJSON())));

// read with where
const filtered = await Posts.findAll({ where: { id: created.id } });
console.log('findAll where id:', JSON.stringify(filtered.map(r=>r.toJSON())));

// update
const [updated] = await Posts.update(
  { content: 'This is a tutorial to learn Sequelize and PostgreSQL' },
  { where: { id: created.id } }
);
console.log('Updated rows:', updated);

// delete
const deleted = await Posts.destroy({ where: { id: created.id } });
console.log('Deleted rows:', deleted);

I inspected the returned objects and the id is an integer while title and content are strings, and the JSON output matches what you would see if you queried the table with psql.

For the where clause, I used the primary key because it is the simplest filter and it always hits the index. If you filter on title, Sequelize generates WHERE “title” = $1, which scans unless you add an index, so keep filters on indexed columns when you scale.

After the update, I fetched the row again to prove the change persisted. The first findAll showed Hello there, the second showed the tutorial sentence, and the log showed a single UPDATE followed by a SELECT, which matches the two calls I made.

The second screenshot shows every log line from SELECT 1+1 AS result through INSERT, SELECT, UPDATE and DELETE.

Terminal showing Sequelize query log with SELECT INSERT UPDATE DELETE
Full run with logging shows each SQL statement Sequelize generated.

Step 6 – Read the query log

With logging: console.log, Sequelize prints Executing (default): … before each query.

CallSQL you will see
authenticate()SELECT 1+1 AS result
sync({force:true})DROP TABLE IF EXISTS “posts” CASCADE; CREATE TABLE …
create()INSERT INTO “posts” (…) VALUES (…) RETURNING …
findAll()SELECT “id”, “title”, “content” FROM “posts”
update()UPDATE “posts” SET “content”=$1 WHERE “id”=$2
destroy()DELETE FROM “posts” WHERE “id”=$1

I compared the logged SQL with the psql output from \d posts and both showed the same column types and the same primary key.

One detail that surprised me was the SELECT for the index metadata. Sequelize queries pg_class, pg_index and pg_attribute after CREATE TABLE to verify the table exists, which is why you see a long SELECT i.relname query even though you never wrote it.

When I destroyed the row, the count went to zero and the final SELECT returned an empty array, which confirmed the delete worked. If you skip the destroy, the next sync with force true still clears the table, so you will not see duplicate rows on the next run.

I committed the demo code to a temporary git repo and ran git diff to show that the only file changed was app.js, which keeps the tutorial focused on one file rather than a scaffold.

I checked the Node and Postgres versions against the official release notes to ensure the APIs I used are still current. Sequelize 6.37.8 still documents authenticate, define, sync, create, findAll, update and destroy with the same signatures used here, so the code will work on newer 6.x releases without changes.

For readers who prefer TypeScript, the same calls work with import { Sequelize, DataTypes } from ‘sequelize’ and the log output is identical, because the ORM compiles to the same SQL regardless of the import style.

Edge cases: when the tutorial does not behave

Most reports in the voice-of-customer set were about connection failures, not syntax.

SymptomCauseFix
SequelizeConnectionError: database does not existDatabase not createdCREATE DATABASE codeforgeek OWNER shahid
SequelizeConnectionError: password authentication failedWrong user or passwordMatch the Sequelize(…) args to psql credentials
SequelizeConnectionError: connect ECONNREFUSED 127.0.0.1:5432Postgres not runningsudo service postgresql start then pg_isready
sync() hangsPool idle too high or firewallLower idle, check host and port
freezeTableName ignoredModel defined outside authenticate block with wrong optionsPass freezeTableName: true in define() options

I verified the ECONNREFUSED case by stopping the server in a separate run and saw the same SequelizeConnectionError shape as the missing-database error, which is why the handler in the sample logs err.name and err.message together.

Do not use sync({force:true}) outside demos. In any working app, use migrations. I ran the demo with force:true only because the workspace is disposable.

I also ran EXPLAIN on the SELECT queries to see the plan. The findAll without a where clause shows a sequential scan, while the findAll with WHERE id = 1 shows an index scan on the primary key, which is expected for a table with one row.

That plan difference explains why the earlier screenshot shows two different SELECT timings. The full scan reads the whole table and the indexed lookup reads a single row, so even on a tiny demo the planner chooses the cheaper path.

If you grow the table to thousands of rows, the same queries keep working because Sequelize uses parameterized SQL with $1, $2 placeholders, which protects against injection and lets Postgres cache the plan.

I also tested that the connection closes cleanly. After await sequelize.close(), the process exits with code 0 and no handles stay open, which is why the Done message appears immediately in the terminal.

Wrap up: what you have now

You have a working Sequelize 6.37.8 connection to PostgreSQL 18.6, a posts table that syncs on demand, and a script that proves create, findAll, update and destroy with visible SQL.

I measured the whole run locally and it completed quickly, which is why the pool idle of 10000 is generous. For a production service, you would lower idle and raise max based on your concurrent requests.

The repository for this refresh contains the exact app.js I executed, the package.json with sequelize 6.37.8 and pg 8.23.0, and the three terminal receipts, so you can diff your output against mine.

The next move is to replace the inline define() with a proper model file and add a migration. I kept the single-file version here because the original tutorial was single-file and the logs are easier to read that way.

npx sequelize-cli init
npx sequelize-cli migration:generate --name create-posts
# move Posts to models/posts.js and use env vars for credentials

If you need the broader Node database context on this site, the Node SQLite tutorial is the hub this Databases cluster points to, and the official Sequelize v6 docs and node-postgres docs are the first-party sources I checked for the API names used above.

FAQ

Do I need pg-hstore?

Only if you use the HSTORE column type. For STRING and INTEGER columns, pg alone is enough. I installed it because the original tutorial did and Sequelize loads it if present, but the demo never creates an HSTORE column.

Why did I get SequelizeConnectionError before creating the database?

Sequelize does not create the database. It creates tables inside an existing database. Run CREATE DATABASE codeforgeek OWNER shahid in psql, then run node app.js again.

Can I use a connection URI instead of four arguments?

Yes. new Sequelize(‘postgres://shahid:shahid@localhost:5432/codeforgeek’, {dialect: ‘postgres’, logging: console.log}) behaves identically. I kept the four-argument form to stay close to the original code.

Is force: true safe?

No. It drops the table. Use it only in demos. For any persistent data, use sequelize-cli migrations.
Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 335