<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Zia Ullah - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Zia Ullah - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 09 Jul 2026 15:53:45 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/ziaullahzia/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Implement Role-Based Access Control in a Node.js REST API with JWT ]]>
                </title>
                <description>
                    <![CDATA[ The first time I built an API without thinking about roles, I gave every logged-in user the same access. It worked fine until a regular user accidentally hit a delete endpoint and wiped test data. Tha ]]>
                </description>
                <link>https://www.freecodecamp.org/news/role-based-access-control-nodejs-rest-api-jwt/</link>
                <guid isPermaLink="false">6a4fb4570140649a4367b476</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Zia Ullah ]]>
                </dc:creator>
                <pubDate>Thu, 09 Jul 2026 14:46:47 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d742efbd-8170-4fb6-8851-1f7c6ef9125e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The first time I built an API without thinking about roles, I gave every logged-in user the same access. It worked fine until a regular user accidentally hit a delete endpoint and wiped test data. That was the day I actually sat down and learned RBAC properly.</p>
<p>Role-Based Access Control sounds fancy, but the idea is simple: what you can do depends on <em>who you are</em>, not just <em>that you're logged in</em>. An admin deletes users. An editor creates posts. A regular user just reads. Same app, completely different experience depending on who's asking.</p>
<p>That's what we're building here. A REST API with three roles: JWT to carry those roles on every request, and a pair of middleware functions that check permissions before your route handlers even run. There's no database hit per request, and no if/else soup in your business logic.</p>
<p>By the end, you'll have three working roles (<code>admin</code>, <code>editor</code>, <code>user</code>) each locked to their own endpoints. More importantly, the pattern is transferable: once it clicks, you'll wire it into your next project without needing a tutorial.</p>
<p><strong>Full source code on GitHub:</strong> <a href="https://github.com/ziaongit/nodejs-rbac-jwt-api">github.com/ziaongit/nodejs-rbac-jwt-api</a></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-well-build">What We'll Build</a></p>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-setting-up-the-in-memory-data-store">Setting Up the In-Memory Data Store</a></p>
</li>
<li><p><a href="#heading-building-the-auth-routes">Building the Auth Routes</a></p>
</li>
<li><p><a href="#heading-building-the-rbac-middleware">Building the RBAC Middleware</a></p>
</li>
<li><p><a href="#heading-building-the-protected-routes">Building the Protected Routes</a></p>
</li>
<li><p><a href="#heading-putting-it-all-together">Putting It All Together</a></p>
</li>
<li><p><a href="#heading-testing-the-api">Testing the API</a></p>
</li>
<li><p><a href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn">What You'll Learn</h2>
<ul>
<li><p>What RBAC is and how it differs from basic authentication</p>
</li>
<li><p>How to embed roles in JWT payloads</p>
</li>
<li><p>How to write reusable Express middleware for token verification and role checking</p>
</li>
<li><p>How to protect API routes based on user roles</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Node.js (v18+) installed</p>
</li>
<li><p>Basic knowledge of Express.js</p>
</li>
<li><p>Familiarity with how JWTs work (we'll cover the relevant parts)</p>
</li>
<li><p>npm installed</p>
</li>
</ul>
<h2 id="heading-what-well-build">What We'll Build</h2>
<p>We'll build a REST API for a simple content management system with three user roles:</p>
<table>
<thead>
<tr>
<th>Role</th>
<th>Permissions</th>
</tr>
</thead>
<tbody><tr>
<td><code>user</code></td>
<td>Read content</td>
</tr>
<tr>
<td><code>editor</code></td>
<td>Read + create content</td>
</tr>
<tr>
<td><code>admin</code></td>
<td>Full access — read, create, delete content, manage users</td>
</tr>
</tbody></table>
<p>The API will expose these endpoints:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Access</th>
</tr>
</thead>
<tbody><tr>
<td>POST</td>
<td>/api/auth/register</td>
<td>Public</td>
</tr>
<tr>
<td>POST</td>
<td>/api/auth/login</td>
<td>Public</td>
</tr>
<tr>
<td>GET</td>
<td>/api/content</td>
<td>user, editor, admin</td>
</tr>
<tr>
<td>POST</td>
<td>/api/content</td>
<td>editor, admin</td>
</tr>
<tr>
<td>DELETE</td>
<td>/api/content/:id</td>
<td>admin only</td>
</tr>
<tr>
<td>GET</td>
<td>/api/admin/users</td>
<td>admin only</td>
</tr>
</tbody></table>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Create a new folder and initialize the project:</p>
<pre><code class="language-bash">mkdir nodejs-rbac-jwt-api
cd nodejs-rbac-jwt-api
npm init -y
</code></pre>
<p>Install the dependencies:</p>
<pre><code class="language-bash">npm install express jsonwebtoken bcryptjs dotenv
npm install --save-dev nodemon
</code></pre>
<p>Here's what each package does:</p>
<ul>
<li><p><strong>express</strong>: web framework for building the API</p>
</li>
<li><p><strong>jsonwebtoken</strong>: creates and verifies JWTs</p>
</li>
<li><p><strong>bcryptjs</strong>: securely hashes passwords</p>
</li>
<li><p><strong>dotenv</strong>: reads your <code>.env</code> file so you're not hardcoding secrets in your source code</p>
</li>
</ul>
<p>Update <code>package.json</code> to add start scripts:</p>
<pre><code class="language-json">"scripts": {
  "start": "node src/app.js",
  "dev": "nodemon src/app.js"
}
</code></pre>
<p>Create the project structure:</p>
<pre><code class="language-plaintext">nodejs-rbac-jwt-api/
├── src/
│   ├── middleware/
│   │   └── auth.js
│   ├── routes/
│   │   ├── auth.js
│   │   ├── content.js
│   │   └── admin.js
│   ├── data/
│   │   └── users.js
│   └── app.js
├── .env
├── .env.example
└── package.json
</code></pre>
<p>Create your <code>.env</code> file:</p>
<pre><code class="language-plaintext">JWT_SECRET=your_super_secret_key_change_this_in_production
PORT=3000
</code></pre>
<p><strong>Important:</strong> Never commit your <code>.env</code> file to version control. Add it to <code>.gitignore</code>.</p>
<h2 id="heading-setting-up-the-in-memory-data-store">Setting Up the In-Memory Data Store</h2>
<p>We don't have a database here, just an array in memory. The point was to keep the focus on RBAC, not spend half the tutorial on database config. In a real project, swap the array for whatever database you're already using.</p>
<p>Create <code>src/data/users.js</code>:</p>
<pre><code class="language-javascript">// In-memory users store
// In production, replace this with a real database (MongoDB, PostgreSQL, etc.)
const users = [];

const findUserByEmail = (email) =&gt; users.find((u) =&gt; u.email === email);
const findUserById = (id) =&gt; users.find((u) =&gt; u.id === id);
const createUser = (user) =&gt; {
  users.push(user);
  return user;
};
const getAllUsers = () =&gt; users.map(({ password, ...user }) =&gt; user);

module.exports = { findUserByEmail, findUserById, createUser, getAllUsers };
</code></pre>
<p>One thing worth noting: <code>getAllUsers</code> uses destructuring to drop the password before returning anything. Never send password fields in API responses, even hashed ones.</p>
<h2 id="heading-building-the-auth-routes">Building the Auth Routes</h2>
<p>The auth routes handle registration and login. Login is where roles first enter the picture — we embed the user's role directly into the JWT payload.</p>
<p>Create <code>src/routes/auth.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { findUserByEmail, createUser } = require('../data/users');

const router = express.Router();

// POST /api/auth/register
router.post('/register', async (req, res) =&gt; {
  const { name, email, password, role } = req.body;

  if (!name || !email || !password) {
    return res.status(400).json({ message: 'Name, email, and password are required' });
  }

  if (findUserByEmail(email)) {
    return res.status(409).json({ message: 'Email already registered' });
  }

  // Only allow valid roles — default to 'user' if none provided
  const validRoles = ['user', 'editor', 'admin'];
  const assignedRole = validRoles.includes(role) ? role : 'user';

  const hashedPassword = await bcrypt.hash(password, 10);

  const newUser = {
    id: Date.now().toString(),
    name,
    email,
    password: hashedPassword,
    role: assignedRole,
  };

  createUser(newUser);

  res.status(201).json({
    message: 'User registered successfully',
    user: {
      id: newUser.id,
      name: newUser.name,
      email: newUser.email,
      role: newUser.role,
    },
  });
});

// POST /api/auth/login
router.post('/login', async (req, res) =&gt; {
  const { email, password } = req.body;

  if (!email || !password) {
    return res.status(400).json({ message: 'Email and password are required' });
  }

  const user = findUserByEmail(email);
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // Issue JWT — embed role in the payload
  const token = jwt.sign(
    {
      id: user.id,
      email: user.email,
      role: user.role,   // ← This is the key part for RBAC
    },
    process.env.JWT_SECRET,
    { expiresIn: '24h' }
  );

  res.json({
    message: 'Login successful',
    token,
  });
});

module.exports = router;
</code></pre>
<p>The most important line is the JWT payload:</p>
<pre><code class="language-javascript">jwt.sign({ id, email, role }, process.env.JWT_SECRET, { expiresIn: '24h' })
</code></pre>
<p>By embedding <code>role</code> in the token, every subsequent request carries the user's permissions without requiring a database lookup. The server just verifies the token and reads the role from the payload.</p>
<h2 id="heading-building-the-rbac-middleware">Building the RBAC Middleware</h2>
<p>This is the core of the system. We need two separate middleware functions:</p>
<ol>
<li><p><code>verifyToken</code> confirms the JWT is valid and attaches the decoded payload to <code>req.user</code></p>
</li>
<li><p><code>checkRole</code> confirms the user has the required role for a specific route</p>
</li>
</ol>
<p>Keeping them separate gives you flexibility. Some routes only need authentication. Others need both authentication and a specific role.</p>
<p>Create <code>src/middleware/auth.js</code>:</p>
<pre><code class="language-javascript">const jwt = require('jsonwebtoken');

// Middleware 1: Verify the JWT token
const verifyToken = (req, res, next) =&gt; {
  const authHeader = req.headers['authorization'];
  const token = authHeader &amp;&amp; authHeader.split(' ')[1]; // Expects: Bearer &lt;token&gt;

  if (!token) {
    return res.status(401).json({ message: 'Access denied. No token provided.' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded; // Attach decoded payload (including role) to request
    next();
  } catch (err) {
    return res.status(403).json({ message: 'Invalid or expired token.' });
  }
};

// Middleware 2: Check if user has one of the required roles
const checkRole = (...allowedRoles) =&gt; {
  return (req, res, next) =&gt; {
    if (!req.user) {
      return res.status(401).json({ message: 'Not authenticated.' });
    }

    if (!allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        message: `Access denied. Required role: ${allowedRoles.join(' or ')}. Your role: ${req.user.role}`,
      });
    }

    next();
  };
};

module.exports = { verifyToken, checkRole };
</code></pre>
<p><code>checkRole</code> uses a rest parameter (<code>...allowedRoles</code>) so you can pass in one or multiple roles:</p>
<pre><code class="language-javascript">checkRole('admin')                  // only admin
checkRole('editor', 'admin')        // editor or admin
checkRole('user', 'editor', 'admin') // all roles
</code></pre>
<p>This makes route definitions clean and readable — the permissions are visible right at the route level.</p>
<h2 id="heading-building-the-protected-routes">Building the Protected Routes</h2>
<p>Now let's wire up routes that use the middleware.</p>
<p>Create <code>src/routes/content.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');

const router = express.Router();

// In-memory content store
const content = [
  { id: '1', title: 'Getting Started with Node.js', author: 'admin' },
  { id: '2', title: 'Express Middleware Explained', author: 'editor' },
];

// GET /api/content — all authenticated users
router.get('/', verifyToken, checkRole('user', 'editor', 'admin'), (req, res) =&gt; {
  res.json({ content });
});

// POST /api/content — editors and admins only
router.post('/', verifyToken, checkRole('editor', 'admin'), (req, res) =&gt; {
  const { title } = req.body;

  if (!title) {
    return res.status(400).json({ message: 'Title is required' });
  }

  const newItem = {
    id: Date.now().toString(),
    title,
    author: req.user.email,
  };

  content.push(newItem);
  res.status(201).json({ message: 'Content created', item: newItem });
});

// DELETE /api/content/:id — admin only
router.delete('/:id', verifyToken, checkRole('admin'), (req, res) =&gt; {
  const index = content.findIndex((c) =&gt; c.id === req.params.id);

  if (index === -1) {
    return res.status(404).json({ message: 'Content not found' });
  }

  content.splice(index, 1);
  res.json({ message: 'Content deleted successfully' });
});

module.exports = router;
</code></pre>
<p>Notice how readable each route is:</p>
<pre><code class="language-javascript">router.delete('/:id', verifyToken, checkRole('admin'), handler)
</code></pre>
<p>You can understand the access control without reading the handler body. This is one of the key advantages of middleware-based RBAC: permissions live at the routing layer, not buried in business logic.</p>
<p>Create <code>src/routes/admin.js</code>:</p>
<pre><code class="language-javascript">const express = require('express');
const { verifyToken, checkRole } = require('../middleware/auth');
const { getAllUsers } = require('../data/users');

const router = express.Router();

// GET /api/admin/users — admin only
router.get('/users', verifyToken, checkRole('admin'), (req, res) =&gt; {
  res.json({ users: getAllUsers() });
});

module.exports = router;
</code></pre>
<h2 id="heading-putting-it-all-together">Putting It All Together</h2>
<p>Create <code>src/app.js</code>:</p>
<pre><code class="language-javascript">require('dotenv').config();
const express = require('express');

const authRoutes = require('./routes/auth');
const contentRoutes = require('./routes/content');
const adminRoutes = require('./routes/admin');

const app = express();

app.use(express.json());

// Routes
app.use('/api/auth', authRoutes);
app.use('/api/content', contentRoutes);
app.use('/api/admin', adminRoutes);

// Health check
app.get('/', (req, res) =&gt; {
  res.json({ message: 'RBAC API is running' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () =&gt; {
  console.log(`Server running on port ${PORT}`);
});
</code></pre>
<h2 id="heading-testing-the-api">Testing the API</h2>
<p>Start the server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<h3 id="heading-step-1-register-users-with-different-roles">Step 1: Register Users with Different Roles</h3>
<p>Register an admin:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Admin User", "email": "admin@example.com", "password": "password123", "role": "admin"}'
</code></pre>
<p>Register an editor:</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Editor User", "email": "editor@example.com", "password": "password123", "role": "editor"}'
</code></pre>
<p>Register a regular user (no role specified — defaults to <code>user</code>):</p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{"name": "Regular User", "email": "user@example.com", "password": "password123"}'
</code></pre>
<h3 id="heading-step-2-log-in-and-get-a-token">Step 2: Log in and Get a Token</h3>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "password123"}'
</code></pre>
<p>You'll get a response like:</p>
<pre><code class="language-json">{
  "message": "Login successful",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
</code></pre>
<p>Copy the token.</p>
<h3 id="heading-step-3-test-role-based-access">Step 3: Test Role-based Access</h3>
<p><strong>Read content as a regular user (should succeed):</strong></p>
<pre><code class="language-bash">curl http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"
</code></pre>
<p><strong>Try creating content as a regular user (should fail — 403):</strong></p>
<pre><code class="language-bash">curl -X POST http://localhost:3000/api/content \
  -H "Authorization: Bearer YOUR_TOKEN_HERE" \
  -H "Content-Type: application/json" \
  -d '{"title": "New Article"}'
</code></pre>
<p>Response:</p>
<pre><code class="language-json">{
  "message": "Access denied. Required role: editor or admin. Your role: user"
}
</code></pre>
<p>Now log in as an editor and try the same POST request. It succeeds. Log in as admin and try the DELETE route. Only the admin token will work.</p>
<h3 id="heading-step-4-decode-the-jwt-to-see-the-role">Step 4: Decode the JWT to See the Role</h3>
<p>You can paste any token into <a href="https://jwt.io">jwt.io</a> to inspect the payload. You'll see something like:</p>
<pre><code class="language-json">{
  "id": "1720300000000",
  "email": "admin@example.com",
  "role": "admin",
  "iat": 1720300000,
  "exp": 1720386400
}
</code></pre>
<p>The <code>role</code> field is exactly what <code>checkRole</code> reads on every protected request.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>Roles live in the JWT payload. The role travels with the token — no extra DB call needed every time someone hits a protected route. It gets embedded at login and verified cryptographically on each request.</p>
<p>Middleware is composable. <code>verifyToken</code> and <code>checkRole</code> are separate, reusable functions. You can chain them on any route in any combination.</p>
<p>Permissions are visible at the route level. <code>router.delete('/:id', verifyToken, checkRole('admin'), handler)</code> tells you everything about access control before you even read the handler.</p>
<p><strong>Before you ship this to production:</strong></p>
<ul>
<li><p>The in-memory array was just to keep this tutorial focused — replace it with a real database before anything goes near production. A server restart wipes all your users right now.</p>
</li>
<li><p>That 24h token expiry is too long. Cut it to 15 minutes and add refresh token rotation. A stolen token becomes useless fast.</p>
</li>
<li><p>Re-validate roles from the DB on sensitive operations. A role change won't reflect in an existing token until it expires</p>
</li>
<li><p>HTTPS, always</p>
</li>
<li><p>If your permission logic grows beyond "check a role", look at <a href="https://casl.js.org/">casl</a>. It handles attribute-level rules cleanly</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The core of it fits in two middleware functions and a JWT payload. I've used this same pattern across several projects. And once you've built it yourself, you'll start spotting it everywhere, because almost every multi-user app needs some version of it.</p>
<p><strong>Full source code on GitHub:</strong> <a href="https://github.com/ziaongit/nodejs-rbac-jwt-api">github.com/ziaongit/nodejs-rbac-jwt-api</a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
