Node, Express, Sequelize, and PostgreSQL Association Example

by Didin J. on Jul 07, 2025 Node, Express, Sequelize, and PostgreSQL Association Example

Learn how to build a Node.js REST API with Express, Sequelize v7, and PostgreSQL using one-to-one, one-to-many, and many-to-many associations.

In this updated tutorial, you'll learn how to build a Node.js REST API using Express, Sequelize v7, and PostgreSQL, with a focus on model associations.

We’ll cover two common types of relationships:

  • One-to-One: A user can have a profile
  • One-to-Many: A user can have many posts

  • Many-to-Many: A post can have many tags, and a tag can belong to many posts

You'll learn how to:

  • Set up Sequelize and connect to PostgreSQL

  • Define models and associations

  • Create RESTful endpoints to manage associated data

  • Query associated models using include

By the end of this tutorial, you'll have a complete working API with properly linked data models.


Setup Environment

Step 1: Initialize Node.js Project

Open your terminal and create a new directory:

mkdir node-express-sequelize-associations
cd node-express-sequelize-associations
npm init -y

Step 2: Install Dependencies

Install the required packages:

npm install express sequelize pg pg-hstore

Install dev dependencies:

npm install --save-dev nodemon

pg is the PostgreSQL client, and pg-hstore is used by Sequelize to handle JSON.

Optional: Add a script to auto-restart using nodemon:

"scripts": {
  "dev": "nodemon server.js"
}

Step 3: Project Structure

We'll follow a simple structure:

node-express-sequelize-associations/
│
├── config/
│   └── db.config.js
│
├── models/
│   ├── index.js
│   ├── user.model.js
│   ├── profile.model.js
│   ├── post.model.js
│   └── tag.model.js
│
├── routes/
│   ├── user.routes.js
│   └── post.routes.js
│
├── controllers/
│   └── user.controller.js
│   └── post.controller.js
│
├── server.js
└── package.json

Step 4: Configure Database Connection

Create config/db.config.js:

module.exports = {
  HOST: "localhost",
  USER: "djamware",
  PASSWORD: "dj@mw@r3",
  DB: "blog_db",
  DIALECT: "postgres"
};

We’ll use this config in the Sequelize instance next.

psql postgres -U djamware
create database blog_db;
\q

You're now ready to set up Sequelize and define models.


Sequelize Configuration and Model Definitions

Step 1: Initialize Sequelize

Create the Sequelize instance in models/index.js.

models/index.js

const { Sequelize, DataTypes } = require('sequelize');
const dbConfig = require('../config/db.config');

const sequelize = new Sequelize(
  dbConfig.DB,
  dbConfig.USER,
  dbConfig.PASSWORD,
  {
    host: dbConfig.HOST,
    dialect: dbConfig.DIALECT,
    logging: false
  }
);

// Initialize models
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;

db.User = require('./user.model')(sequelize, DataTypes);
db.Profile = require('./profile.model')(sequelize, DataTypes);
db.Post = require('./post.model')(sequelize, DataTypes);
db.Tag = require('./tag.model')(sequelize, DataTypes);
db.PostTag = require('./posttag.model')(sequelize, DataTypes);

// Associations

// One-to-One: User ↔ Profile
db.User.hasOne(db.Profile, { foreignKey: 'userId', as: 'profile' });
db.Profile.belongsTo(db.User, { foreignKey: 'userId', as: 'user' });

// One-to-Many: User → Posts
db.User.hasMany(db.Post, { foreignKey: 'userId', as: 'posts' });
db.Post.belongsTo(db.User, { foreignKey: 'userId', as: 'user' });

// Many-to-Many: Post ↔ Tag (via PostTag)
db.Post.belongsToMany(db.Tag, {
  through: db.PostTag,
  as: 'tags',
  foreignKey: 'postId'
});
db.Tag.belongsToMany(db.Post, {
  through: db.PostTag,
  as: 'posts',
  foreignKey: 'tagId'
});

module.exports = db;

Step 2: Define Models

📄 models/user.model.js

module.exports = (sequelize, DataTypes) => {
  return sequelize.define('User', {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    name: DataTypes.STRING,
    email: DataTypes.STRING
  });
};

📄 models/profile.model.js

module.exports = (sequelize, DataTypes) => {
  return sequelize.define('Profile', {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    bio: DataTypes.TEXT,
    userId: {
      type: DataTypes.INTEGER,
      allowNull: false
    }
  });
};

📄 models/post.model.js

module.exports = (sequelize, DataTypes) => {
  return sequelize.define('Post', {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    title: DataTypes.STRING,
    content: DataTypes.TEXT,
    userId: {
      type: DataTypes.INTEGER,
      allowNull: false
    }
  });
};

📄 models/tag.model.js

module.exports = (sequelize, DataTypes) => {
  return sequelize.define('Tag', {
    id: {
      type: DataTypes.INTEGER,
      primaryKey: true,
      autoIncrement: true
    },
    name: DataTypes.STRING
  });
};

📄 models/posttag.model.js (Join Table)

module.exports = (sequelize, DataTypes) => {
  return sequelize.define('PostTag', {
    postId: {
      type: DataTypes.INTEGER,
      references: { model: 'Posts', key: 'id' }
    },
    tagId: {
      type: DataTypes.INTEGER,
      references: { model: 'Tags', key: 'id' }
    }
  });
};

Step 3: Sync Models to Database

In server.js, initialize the database:

const express = require('express');
const app = express();
const db = require('./models');

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Sync database
db.sequelize.sync({ force: true }).then(() => {
  console.log('Database synced (tables recreated)');
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}.`);
});

🔁 force: true drops and recreates tables on every restart. Use cautiously in production!

Models and associations are now ready.


Creating Controllers and Routes to Manage the Associations

Let's now build the controllers and routes to handle CRUD operations and manage the associations between models.

We'll implement:

  • User CRUD with Profile (1:1)

  • Post CRUD with User (1:N)

  • Tag CRUD and tag assignment to Posts (N:M)

Step 1: Create Controllers

📄 controllers/user.controller.js

const db = require('../models');
const { User, Profile } = db;

exports.createUser = async (req, res) => {
  try {
    const user = await User.create(req.body, { include: ['profile'] });
    res.json(user);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

exports.getAllUsers = async (req, res) => {
  try {
    const users = await User.findAll({ include: ['profile', 'posts'] });
    res.json(users);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

✅ This handles user creation and fetches all users with their profile (1:1) and posts (1:N).

📄 controllers/post.controller.js

const db = require('../models');
const { Post, User, Tag } = db;

exports.createPost = async (req, res) => {
  try {
    const post = await Post.create(req.body);
    res.json(post);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

exports.getAllPosts = async (req, res) => {
  try {
    const posts = await Post.findAll({
      include: ['user', 'tags']
    });
    res.json(posts);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

exports.addTagToPost = async (req, res) => {
  try {
    const { postId, tagId } = req.body;
    const post = await Post.findByPk(postId);
    const tag = await Tag.findByPk(tagId);
    if (post && tag) {
      await post.addTag(tag);
      res.json({ message: 'Tag added to post.' });
    } else {
      res.status(404).json({ message: 'Post or Tag not found.' });
    }
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

📄 controllers/tag.controller.js

const db = require('../models');
const { Tag } = db;

exports.createTag = async (req, res) => {
  try {
    const tag = await Tag.create(req.body);
    res.json(tag);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

exports.getAllTags = async (req, res) => {
  try {
    const tags = await Tag.findAll();
    res.json(tags);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
};

Step 2: Create Routes

📄 routes/user.routes.js

const express = require('express');
const router = express.Router();
const userController = require('../controllers/user.controller');

router.post('/', userController.createUser);
router.get('/', userController.getAllUsers);

module.exports = router;

📄 routes/post.routes.js

const express = require('express');
const router = express.Router();
const postController = require('../controllers/post.controller');

router.post('/', postController.createPost);
router.get('/', postController.getAllPosts);
router.post('/add-tag', postController.addTagToPost);

module.exports = router;

📄 routes/tag.routes.js

const express = require('express');
const router = express.Router();
const tagController = require('../controllers/tag.controller');

router.post('/', tagController.createTag);
router.get('/', tagController.getAllTags);

module.exports = router;

Step 3: Register Routes in server.js

const express = require('express');
const app = express();
const db = require('./models');

app.use(express.json());

// Sync DB
db.sequelize.sync({ force: true }).then(() => {
  console.log('Database synced');
});

app.use('/api/users', require('./routes/user.routes'));
app.use('/api/posts', require('./routes/post.routes'));
app.use('/api/tags', require('./routes/tag.routes'));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

API Test Summary

Method URL Description
POST /api/users Create user + profile
GET /api/users Get all users with profile and posts
POST /api/posts Create post (with userId)
GET /api/posts Get all posts with user and tags
POST /api/posts/add-tag Assign tag to post
POST /api/tags Create tag
GET /api/tags List all tags


Final Testing Guide (with curl & Postman)

Make sure your server is running:

npm run dev

1️⃣ Create a User with Profile (One-to-One)

🟦 POST /api/users

Body:

{
  "name": "John Doe",
  "email": "[email protected]",
  "profile": {
    "bio": "Software developer from Jakarta."
  }
}

curl:

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"[email protected]","profile":{"bio":"Software developer from Jakarta."}}'

2️⃣ Create a Post for a User (One-to-Many)

🟦 POST /api/posts

Body:

{
  "title": "Sequelize Associations",
  "content": "This is a guide on Sequelize associations.",
  "userId": 1
}

curl:

curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"Sequelize Associations","content":"This is a guide on Sequelize associations.","userId":1}'

3️⃣ Create Tags (for Many-to-Many)

🟦 POST /api/tags

Body:

{ "name": "sequelize" }

You can repeat this for another tag:

{ "name": "nodejs" }

curl:

curl -X POST http://localhost:3000/api/tags \
  -H "Content-Type: application/json" \
  -d '{"name":"sequelize"}'

4️⃣ Assign Tag to Post (Many-to-Many)

🟦 POST /api/posts/add-tag

Body:

{
  "postId": 1,
  "tagId": 1
}

curl:

curl -X POST http://localhost:3000/api/posts/add-tag \
  -H "Content-Type: application/json" \
  -d '{"postId":1,"tagId":1}'

5️⃣ Get All Users with Profile and Posts

🟩 GET /api/users

curl:

curl http://localhost:3000/api/users

You’ll get data like:

[
  {
    "id": 1,
    "name": "John Doe",
    "email": "[email protected]",
    "profile": {
      "bio": "Software developer from Jakarta."
    },
    "posts": [...]
  }
]

6️⃣ Get All Posts with User and Tags

🟩 GET /api/posts

curl:

curl http://localhost:3000/api/posts

You’ll see:

[
  {
    "id": 1,
    "title": "Sequelize Associations",
    "user": { "name": "John Doe" },
    "tags": [{ "name": "sequelize" }]
  }
]

Final Tip for Postman Users

You can use Postman to send the same requests with:

  • Headers → Content-Type: application/json

  • Method → POST/GET

  • Body → raw JSON


Conclusion

In this updated tutorial, you’ve learned how to build a complete RESTful API using Node.js, Express, Sequelize v7, and PostgreSQL, focusing on defining and managing model associations.

We covered the three core relationship types:

  • One-to-One: UserProfile

  • One-to-Many: UserPost

  • Many-to-Many: PostTag

You created models, configured associations, built controllers and routes, and tested everything using curl or Postman. This foundation allows you to build more advanced APIs with nested data, eager loading, and relational integrity using Sequelize’s powerful ORM capabilities.


Key Takeaways

  • Sequelize v7 supports modern association definitions with strong TypeScript and SQL-type support.

  • Use hasOne, hasMany, belongsToMany, and belongsTo to create clean relational mappings.

  • Always test your associations using .findAll({ include }) to verify eager loading works correctly.

  • Use a join table (e.g., PostTag) for many-to-many relations and manage them via .addX() helpers.

  • Group logic into controllers and use Express routes for clean API organization.

You're now equipped to confidently model real-world relational data in modern JavaScript web applications!

You can get the full working source code on our GitHub.

That is just the basics. If you need more deep learning about Node, Express, Sequelize, PostgreSQL/MySQL, or related, you can take the following cheap course:

Thanks!