Create a JWT Authentication API in Node.js

A JSON Web Token (JWT) API issues a short-lived token after your application has authenticated an account, then verifies it before a protected route runs. I ran JWT_SECRET=development-only-secret node test.mjs on Node v24.18.0 with express 5.2.1 and jsonwebtoken 9.0.3, and the saved terminal receipt returned 201 for token issuance, 200 for an accepted bearer token, and 401 when the header was absent.

What this API proves

The login route accepts a userId only so the token flow stays visible, and an application must replace that demonstration input with a user lookup and password-hash verification before signing a token.

The token carries a subject claim, expiry, issuer, and audience, which the protected route verifies with the HS256 algorithm selected by the API.

Create the Node.js project

Create a directory, initialize npm, and install Express with jsonwebtoken. Use a long random value for JWT_SECRET in your deployment environment instead of committing a secret to the project.

npm init -y
npm install express jsonwebtoken

Issue a short-lived token

Save the following file as app.mjs. The login handler returns 400 when userId is missing, then creates a token whose sub claim identifies the authenticated account.

import express from "express";
import jwt from "jsonwebtoken";

const app = express();
app.use(express.json());

const issuer = "example-auth-api";
const audience = "example-client";
const secret = process.env.JWT_SECRET;

if (!secret) {
  throw new Error("JWT_SECRET must be set before starting the API");
}

function createAccessToken(userId) {
  return jwt.sign({ sub: userId }, secret, {
    algorithm: "HS256",
    audience,
    expiresIn: "15m",
    issuer,
  });
}

function requireAccessToken(req, res, next) {
  const authorization = req.get("authorization");
  const token = authorization?.startsWith("Bearer ")
    ? authorization.slice(7)
    : undefined;

  if (!token) {
    return res.status(401).json({ error: "Bearer token required" });
  }

  try {
    req.auth = jwt.verify(token, secret, {
      algorithms: ["HS256"],
      audience,
      issuer,
    });
    next();
  } catch {
    res.status(401).json({ error: "Invalid or expired token" });
  }
}

app.post("/login", (req, res) => {
  const { userId } = req.body;

  if (typeof userId !== "string" || userId.length === 0) {
    return res.status(400).json({ error: "userId is required" });
  }

  // Replace this demonstration input with a verified user record and password check.
  res.status(201).json({ accessToken: createAccessToken(userId) });
});

app.get("/profile", requireAccessToken, (req, res) => {
  res.json({ message: "Protected response", userId: req.auth.sub });
});

export default app;

Verify the bearer token

The requireAccessToken middleware reads the Authorization header, removes the Bearer prefix, and passes the token to jwt.verify. Verification checks the signature, expiry, issuer, audience, and permitted algorithm before the profile handler can read req.auth.sub.

A JSON Web Token is a signed claim set, not an encrypted password container. Avoid placing passwords, access keys, or private profile data in its payload because anyone holding the token can decode its contents.

Run the API test

The test starts the Express app on an ephemeral local port and exercises the issue, accepted-token, and missing-token paths. Save it as test.mjs beside app.mjs, then run it with JWT_SECRET set for the process.

import app from "./app.mjs";

const server = app.listen(0, "127.0.0.1");
await new Promise((resolve) => server.once("listening", resolve));
const { port } = server.address();
const baseUrl = `http://127.0.0.1:${port}`;

try {
  const login = await fetch(`${baseUrl}/login`, {
    body: JSON.stringify({ userId: "ada" }),
    headers: { "content-type": "application/json" },
    method: "POST",
  });
  const { accessToken } = await login.json();

  const profile = await fetch(`${baseUrl}/profile`, {
    headers: { authorization: `Bearer ${accessToken}` },
  });

  const missingToken = await fetch(`${baseUrl}/profile`);

  console.log("POST /login", login.status);
  console.log("GET /profile with token", profile.status, await profile.json());
  console.log("GET /profile without token", missingToken.status, await missingToken.json());
} finally {
  await new Promise((resolve, reject) =>
    server.close((error) => (error ? reject(error) : resolve())),
  );
}

Run the test command from the project directory.

JWT_SECRET=development-only-secret node test.mjs
Terminal output from the Express JWT API test showing 201, 200, and 401 responses
The verified test issues a token, accepts it on a protected route, and rejects a request without a bearer token.

JWT boundaries to keep in your API

Use a short expiration and verify issuer, audience, and algorithms every time you accept a token. The jsonwebtoken project documents these options, and RFC 7519 defines the registered claims used by JWTs.

  • Keep the signing secret in your deployment secret store or environment, never in source control.
  • Authenticate the account before you call jwt.sign. Token signing does not validate a password or prove an account exists.
  • Use HTTPS when a client sends an Authorization header, and return 401 for missing, expired, or invalid tokens.

Where OAuth fits

JWT verification protects a route after your application has issued or accepted a token. If you need delegated access through a provider such as Google, follow the OAuth 2.0 flow before deciding where your API validates the resulting identity.

Add the authentication boundary

Put your user lookup and password verification immediately before createAccessToken, then keep the three-route test in your API suite. That boundary prevents the sample login endpoint from becoming an accidental authentication system.

For token claim details, read RFC 7519 and the jsonwebtoken documentation alongside your application requirements.

Aditya Gupta
Aditya Gupta

Aditya Gupta is a founding member and editor at CodeForGeek. He first found his way into tech by reading articles, and now writes approachable guides to Node.js security, authentication, AI tools, coding agents, and web scraping.

Articles: 529