Node.js Development

Understanding and Implementing a Simple REST API with Node.js and OAuth 2.0

By the Domain India teamPublished 8 min read
Knowledge base article
Contents (8 sections)

A REST API lets other programs create, read, update and delete your data over HTTP. OAuth 2.0 decides who may call it: callers present an access token, and your API checks that token before doing anything. This guide builds a small CRUD API with Node.js and Express, then protects it with OAuth 2.0 access tokens the way current practice recommends.

Key takeaways

Build the API with Express and let a dedicated authorization server (an identity provider such as Keycloak, Auth0, Okta or Microsoft Entra ID) issue the tokens. Your API is the resource server: it verifies each JWT access token's signature, issuer, audience, expiry and scopes with a maintained library such as jose. Use the Authorization Code flow with PKCE for users and Client Credentials for machine-to-machine calls; the old implicit and password grants should not be used.

1. REST and OAuth 2.0 in two minutes

A REST API maps HTTP methods to actions on resources:

Method and pathActionSuccess code
POST /itemsCreate an item201 Created
GET /items/:idRead one item200 OK
PUT /items/:idReplace an item200 OK
DELETE /items/:idDelete an item204 No Content

OAuth 2.0 has three roles you need to keep apart:

  • Client: the app calling your API, such as a web app, mobile app or backend job.
  • Authorization server: logs users in, and issues access tokens.
  • Resource server: your API. It never sees passwords; it only checks tokens.

The most common mistake is to build all three into one Express app. Writing a correct authorization server is hard and security-critical, so use an existing one.

2. Choose the right flow

Who is callingFlow to useNotes
A user in a browser or mobile appAuthorization Code with PKCEPKCE is now recommended for every client, not only mobile apps
A backend service or cron jobClient CredentialsNo user; the service authenticates with its own ID and secret
A TV or CLI with no browserDevice AuthorizationThe user approves on another device

The implicit grant and the resource owner password grant are no longer recommended. The OAuth 2.0 Security Best Current Practice (RFC 9700) and the OAuth 2.1 draft drop them, so don't build new code on either.

3. Set up the project

Use a current LTS release of Node.js (Node.js 22 or 24 in 2026) and Express 5.

bash
mkdir items-api && cd items-api
npm init -y
npm pkg set type=module
npm install express jose

Express has its own JSON body parser (express.json()), so the separate body-parser package from older tutorials isn't needed.

4. Build the CRUD API

This version keeps data in memory, which is enough to learn with. A Map is used instead of a plain object, so a client can't overwrite built-in properties by sending an ID such as __proto__.

js
// server.js
import express from 'express';
import { randomUUID } from 'node:crypto';
import { requireAuth, requireScope } from './auth.js';

const app = express();
app.use(express.json({ limit: '100kb' }));

const items = new Map();

app.post('/items', requireAuth, requireScope('items:write'), (req, res) => {
  const { name } = req.body ?? {};
  if (typeof name !== 'string' || name.length === 0 || name.length > 200) {
    return res.status(400).json({ error: 'name must be a string of 1-200 characters' });
  }
  const id = randomUUID();
  items.set(id, { id, name, owner: req.auth.sub });
  res.status(201).location(`/items/${id}`).json(items.get(id));
});

app.get('/items/:id', requireAuth, requireScope('items:read'), (req, res) => {
  const item = items.get(req.params.id);
  if (!item) return res.status(404).json({ error: 'not found' });
  res.json(item);
});

app.put('/items/:id', requireAuth, requireScope('items:write'), (req, res) => {
  const item = items.get(req.params.id);
  if (!item) return res.status(404).json({ error: 'not found' });
  const { name } = req.body ?? {};
  if (typeof name !== 'string' || name.length === 0 || name.length > 200) {
    return res.status(400).json({ error: 'name must be a string of 1-200 characters' });
  }
  item.name = name;
  res.json(item);
});

app.delete('/items/:id', requireAuth, requireScope('items:write'), (req, res) => {
  if (!items.delete(req.params.id)) return res.status(404).json({ error: 'not found' });
  res.status(204).end();
});

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

The server creates the ID itself with randomUUID(), rather than trusting one from the client, and every input is checked before it is stored.

5. Verify access tokens

Most identity providers issue access tokens as signed JWTs and publish their public keys at a JWKS URL. The jose library fetches and caches those keys and checks the token for you.

js
// auth.js
import { createRemoteJWKSet, jwtVerify } from 'jose';

const ISSUER   = process.env.OAUTH_ISSUER;    // e.g. https://auth.example.com/
const AUDIENCE = process.env.OAUTH_AUDIENCE;  // the identifier of this API
const JWKS = createRemoteJWKSet(new URL(process.env.OAUTH_JWKS_URL));

export async function requireAuth(req, res, next) {
  const [scheme, token] = (req.headers.authorization ?? '').split(' ');
  if (scheme !== 'Bearer' || !token) {
    return res.status(401).set('WWW-Authenticate', 'Bearer').json({ error: 'missing token' });
  }
  try {
    const { payload } = await jwtVerify(token, JWKS, { issuer: ISSUER, audience: AUDIENCE });
    req.auth = payload;
    next();
  } catch {
    res.status(401).set('WWW-Authenticate', 'Bearer error="invalid_token"')
       .json({ error: 'invalid token' });
  }
}

export function requireScope(scope) {
  return (req, res, next) => {
    const scopes = String(req.auth?.scope ?? '').split(' ');
    if (!scopes.includes(scope)) return res.status(403).json({ error: 'insufficient scope' });
    next();
  };
}

jwtVerify checks the signature, the issuer, the audience and the expiry time. Checking the audience matters: without it, a token issued for a different API would be accepted by yours. Some providers put scopes in a scp or permissions claim instead of scope; check your provider's token format.

If your provider issues opaque (non-JWT) tokens, call its token introspection endpoint (RFC 7662) instead of jwtVerify, and cache the result briefly.

6. Test it

Register the API and a machine-to-machine client with your identity provider, then request a token with the Client Credentials flow. The token URL and any extra parameter (some providers need audience or resource) come from your provider's documentation:

bash
curl -s -X POST https://auth.example.com/oauth/token \
  -d grant_type=client_credentials \
  -d client_id="$CLIENT_ID" -d client_secret="$CLIENT_SECRET" \
  -d scope="items:read items:write"

Call the API with the access_token from the response:

bash
curl -s -X POST http://localhost:3000/items \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"First item"}'

A request with no token should return 401, and a token without the right scope should return 403. Test both.

7. Before you go live

HTTPS only
Tokens are bearer credentials: anyone who captures one can use it until it expires.
Short-lived tokens
Keep access tokens to minutes, not days, and use refresh tokens in the client.
Never log tokens
Keep the Authorization header out of logs and error reports.
Store data properly
Replace the in-memory Map with a database, and use parameterised queries.
Limit abuse
Add rate limiting and a request size limit, and set CORS to known origins only.
Keep secrets out of code
Read issuer, audience and client secrets from environment variables.

If you truly need to run your own authorization server, use a maintained, certified implementation such as Keycloak or oidc-provider, not hand-written token endpoints.

8. Running this on Domain India

WhereHow the API runsGood for
cPanel or DirectAdmin shared hostingSetup Node.js App (cPanel offers Node.js 20, 22 and 24); the app runs behind ApacheSmall, low-traffic APIs next to an existing site
App PlatformNode.js apps are detected automatically; other stacks need a DockerfileProduction APIs with deploys from Git or CI
VPSSelf-managed, full root access; you run Node.js, a process manager and a reverse proxyFull control and your own identity server

On shared hosting, jailed SSH access is available on request (it is off by default; ask support), and your app shares the account's CPU and memory limits. See deploying a Node.js app on shared hosting and getting started with the App Platform. App Platform prices on the card are live and exclude 18% GST.

App Starter
₹100/mo + GST
  • 512 MB RAM per app
  • 1 vCPU
  • 5 GB NVMe SSD
  • PostgreSQL Database
See plan details
Do I need to build my own OAuth 2.0 server for my API?

Usually not. Your API only needs to verify access tokens. Let an identity provider or a maintained server such as Keycloak issue them, and verify them in the API with a library such as jose.

Which OAuth 2.0 flow should a single-page app or mobile app use?

The Authorization Code flow with PKCE. The implicit grant is no longer recommended for any client.

Which flow should a backend service use to call my API?

The Client Credentials flow. The service authenticates with its own client ID and secret and receives an access token without any user involved.

What should my API check in a JWT access token?

The signature against the provider's published keys, the issuer, the audience, the expiry time, and the scopes the endpoint needs. Reject the request with 401 for a bad token and 403 for a missing scope.

Is express-oauth-server still a good choice?

For new projects, no. It turns your API into an authorization server you have to secure yourself. Use an identity provider for tokens and verify them in your API with a maintained library.

Can I host a Node.js API on Domain India?

Yes. Small APIs can run on cPanel or DirectAdmin shared hosting through the Node.js app tool, production APIs suit the App Platform, where Node.js is detected automatically, and a self-managed VPS gives full control.

Ready to deploy your API? Compare App Platform plans, run it on cPanel hosting for small projects, or choose a VPS for full control.

Deploy your Node.js API

Node.js apps are detected automatically; other stacks deploy with a Dockerfile.

See App Platform plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Build a REST API with Node.js and OAuth 2.0