Secure Node.js, Express, and PostgreSQL REST API with JWT and Optional OAuth2 (2025 Edition)

by Didin J. on Jul 18, 2025 Secure Node.js, Express, and PostgreSQL REST API with JWT and Optional OAuth2 (2025 Edition)

Build a secure REST API using Node.js, Express, PostgreSQL with JWT authentication, Sequelize ORM, and optional OAuth2 support for modern web apps.

The comprehensive step-by-step tutorial on building a secure Node.js, Express.js, Passport.js, and PostgreSQL Restful Web Service. Previously, we have shown you a combination of Node.js, Express.js, and PostgreSQL tutorial. Now, we just add security for those REST API Web Service endpoints. Of course, we will start this tutorial from scratch, or a zero application. We will use JWT for this Node.js, Express.js, Passport.js, and PostgreSQL tutorial.


🧰 Prerequisites

  • Node.js 20+

  • PostgreSQL 14+

  • Basic knowledge of Express.js & REST APIs

  • curl or Postman for testing


Step 1: Initialize Your Project

Using Express.js with a myriad of HTTP utility methods and middleware at your disposal, creating a robust API is quick and easy. Go to your Node project folder, then create a new Node application folder.

mkdir secure-node-api && cd secure-node-api
npm init -y

Enable ES Module Support

In your package.json, add/modify:

"type": "module",

Install Dependencies

npm install express sequelize pg pg-hstore passport passport-jwt jsonwebtoken bcrypt dotenv
npm install -D nodemon sequelize-cli


Step 2: Set up Sequelize

Sequelize is a promise-based Node.js ORM for Postgres, MySQL, MariaDB, SQLite, and Microsoft SQL Server. It features solid transaction support, relations, eager and lazy loading, read replication and more. Before installing the modules for this project, first, initialize Sequelize.

npx sequelize-cli init

Configure config/config.json:

{
  "development": {
    "username": "djamware",
    "password": "dj@mw@r3",
    "database": "secure_api_db",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}

Create Database

psql postgres -U djamware
create database secure_api_db;
\q

Create User Model

npx sequelize-cli model:generate --name User --attributes username:string,password:string
npx sequelize-cli db:migrate

Update models/index.js for ESM

Edit models/index.js:

import { Sequelize } from 'sequelize';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
dotenv.config();

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/config.json')))[env];

const sequelize = new Sequelize(config.database, config.username, config.password, config);

const db = {};

const modelFiles = fs
  .readdirSync(__dirname)
  .filter(
    file => file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'
  );

for (const file of modelFiles) {
  const model = await import(path.join(__dirname, file));
  const definedModel = model.default(sequelize, Sequelize.DataTypes);
  db[definedModel.name] = definedModel;
}

Object.keys(db).forEach(modelName => {
  if (db[modelName].associate) {
    db[modelName].associate(db);
  }
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

export default db;

Update models/user.js for ESM

Edit models/user.js:

export default (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    username: DataTypes.STRING,
    password: DataTypes.STRING
  });

  User.associate = function(models) {
    // associations can be defined here
  };

  return User;
};


Step 3: Auth Helpers (Password Hashing)

Create utils/auth.js:

import bcrypt from 'bcrypt';
const SALT_ROUNDS = 10;
export const hashPassword = pw => bcrypt.hash(pw, SALT_ROUNDS);
export const comparePassword = (pw, hash) => bcrypt.compare(pw, hash);


Step 4: Configure Passport JWT

Create config/passport.js:

import passport from 'passport';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import { User } from '../models/index.js';
import dotenv from 'dotenv';
dotenv.config();

passport.use(new JwtStrategy({
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: process.env.JWT_SECRET
}, async (payload, done) => {
  try {
    const user = await User.findByPk(payload.id);
    return done(null, user || false);
  } catch (err) {
    return done(err, false);
  }
}));

export default passport;

Create .env file:

PORT=3000
JWT_SECRET=your_super_secret_jwt_key


Step 5: Auth Routes

Create routes/auth.js:

import express from 'express';
import jwt from 'jsonwebtoken';
import passport from '../config/passport.js';
import db from '../models/index.js';
const { User } = db;
import { hashPassword, comparePassword } from '../utils/auth.js';
import dotenv from 'dotenv';
dotenv.config();

const router = express.Router();

router.post('/signup', async (req, res) => {
  const { username, password } = req.body;
  const hashed = await hashPassword(password);
  const user = await User.create({ username, password: hashed });
  res.json({ id: user.id, username: user.username });
});

router.post('/signin', async (req, res) => {
  const { username, password } = req.body;
  const user = await User.findOne({ where: { username } });
  if (!user || !(await comparePassword(password, user.password))) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }
  const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
});

router.get('/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
  res.json({ id: req.user.id, username: req.user.username });
});

export default router;


Step 6: Create App Entry

Create app.js:

import express from 'express';
import passport from './config/passport.js';
import authRoutes from './routes/auth.js';
import dotenv from 'dotenv';
dotenv.config();

const app = express();
app.use(express.json());
app.use(passport.initialize());
app.use('/api/auth', authRoutes);

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

Start with:

npx nodemon app.js


(Optional) OAuth2 Integration

Add OAuth2 dependencies:

npm install express-oauth-server

Integrate OAuth2 following this structure:

import OAuthServer from 'express-oauth-server';
app.oauth = new OAuthServer({ model: require('./oauthModel.js') });

app.post('/oauth/token', app.oauth.token());
app.get('/secure', app.oauth.authenticate(), (req, res) => res.send('Secure Data'));

Create oauthModel.js (Optional OAuth2 Support)

Create oauthModel.js:

export default {
  getAccessToken: async function(token) {
    // Implement token retrieval logic
    return { accessToken: token, client: {}, user: {}, accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000) };
  },

  getClient: async function(clientId, clientSecret) {
    return { id: clientId, grants: ['password'] };
  },

  saveToken: async function(token, client, user) {
    return { ...token, client, user };
  },

  getUser: async function(username, password) {
    // Validate and return a user
    return { id: 1, username };
  }
};


Testing Endpoints

Use Postman or curl:

curl -X POST http://localhost:3000/api/auth/signup -H "Content-Type: application/json" -d '{"username":"test","password":"1234"}'

Response:

{"id":1,"username":"admin"}

Sign in and get JWT, then access the profile:

curl -X POST http://localhost:3000/api/auth/signin \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

Response:

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNzUyODM3NzAxLCJleHAiOjE3NTI4NDEzMDF9.d2d2KHB5mF_bPhelTafp7g8ScTnzbEX7JL8pITEg1uw"}

Access secure route:

curl http://localhost:3000/api/auth/profile \
  -H "Authorization: Bearer <your_token_here>"

Response:

{"id":1,"username":"admin"}

Test OAuth2 token grant (optional):

curl -X POST http://localhost:3000/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=password&username=admin&password=123456&client_id=abc&client_secret=123"


Conclusion

You've now created a secure, modern REST API using Node.js, Express, PostgreSQL, Sequelize, JWT, and optionally OAuth2. Ready to integrate into any frontend or mobile app!

You can get the working source code on our GitHub.

That's 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!