Tutorials55 min read

Node.js API Guide 2026: REST, GraphQL, gRPC & WebSockets Mastery

The complete Node.js API reference. Build REST, GraphQL, gRPC, and WebSocket APIs. Production code, security, and deployment with 100+ examples.

Dev Kant Kumar
Dev Kant Kumar
March 4, 2026
2026 Production Guide

Node.js API Mastery

REST · GraphQL · gRPC · WebSockets · 25 Topics · 100+ Code Examples

DK
Dev Kant Kumar
55 min read
Updated March 2026

What is an API?

An API (Application Programming Interface) is a contract between two software systems that defines how they communicate - what requests can be made, what data to send, and what responses to expect.

Think of it as a waiter in a restaurant: you (the client) don't go into the kitchen (the server) directly. You tell the waiter (the API) what you want, and the waiter brings back the result.

Key Terms

Client - The consumer making requests (browser, mobile app, another server)
Server - The system exposing functionality through the API
Endpoint - A specific URL/address that handles a type of request
Request - Data sent from client to server (method, headers, body, params)
Response - Data returned from server to client (status code, headers, body)
Protocol - The rules governing communication (HTTP, TCP, WebSocket)
Payload - The actual data inside a request or response body
Serialization - Converting data to a transferable format (JSON, XML, Protobuf)

Why APIs Matter in Production

Decoupling

Frontend and backend evolve independently

Reusability

One API serves web, mobile, and third-party integrations

Scalability

Each API service scales independently

Security

Controlled access to internal business logic

Interoperability

Systems in different languages can communicate

How the Web Works Under the Hood

Before writing a single line of API code, you must understand what happens when a request travels across the internet.

The Journey of an API Request

BASH
Client (Node.js / Browser)
    │
    ▼
DNS Resolution  →  IP Address lookup for domain
    │
    ▼
TCP Handshake   →  SYN → SYN-ACK → ACK (3-way)
    │
    ▼
TLS Handshake   →  Certificate exchange, session keys (HTTPS)
    │
    ▼
HTTP Request    →  Sent over the encrypted TCP connection
    │
    ▼
Server receives →  Load Balancer → App Server → Handler
    │
    ▼
HTTP Response   →  Travels back through same connection
    │
    ▼
Client receives →  Parses JSON / handles data

OSI Model - Relevant Layers

LayerNameWhat You Interact With
7ApplicationHTTP, WebSocket, gRPC, DNS
6PresentationTLS/SSL, JSON/XML encoding
4TransportTCP, UDP (ports, reliability)
3NetworkIP addresses, routing
1-2Physical/Data LinkHardware (not your concern)

DNS in Practice (Node.js)

JAVASCRIPT
const dns = require('dns').promises;

// Lookup IP for a hostname
const result = await dns.lookup('api.example.com');
console.log(result); // { address: '93.184.216.34', family: 4 }

HTTP Deep Dive

HTTP is the backbone of virtually every API. Mastering it is non-negotiable.

HTTP/1.1 vs HTTP/2 vs HTTP/3

FeatureHTTP/1.1HTTP/2HTTP/3
TransportTCPTCPQUIC (UDP-based)
Multiplexing❌ (one req/connection)✅ (multiple streams)
Header Compression✅ HPACK✅ QPACK
Server Push
Head-of-line blockingYesSolved at HTTP layerFully solved
TLS RequiredOptionalPractically requiredBuilt-in

Node.js HTTP/2 Example

JAVASCRIPT
const http2 = require('http2');
const fs = require('fs');

const server = http2.createSecureServer({
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
});

server.on('stream', (stream, headers) => {
  stream.respond({ ':status': 200, 'content-type': 'application/json' });
  stream.end(JSON.stringify({ message: 'HTTP/2 response' }));
});

server.listen(8443);

HTTP Methods

MethodPurposeIdempotentSafeHas Body
GETRetrieve resource
POSTCreate resource
PUTReplace entire resource
PATCHPartial update
DELETERemove resourceOptional
HEADGET without body
OPTIONSDescribe capabilities

Idempotent vs Safe

Idempotent = calling it N times has the same effect as calling it once.

Safe = does not modify server state.

HTTP Status Codes - Complete Reference

2xx - Success

  • 200 OK
  • 201 Created (POST success)
  • 202 Accepted (async job)
  • 204 No Content (DELETE)
  • 206 Partial Content (streaming)

3xx - Redirection

  • 301 Moved Permanently
  • 302 Found (temporary)
  • 304 Not Modified (cache hit)
  • 307 Temporary Redirect
  • 308 Permanent Redirect

4xx - Client Errors

  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 409 Conflict
  • 422 Unprocessable Entity
  • 429 Too Many Requests

5xx - Server Errors

  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout

Essential HTTP Headers

Request Headers

BASH
Authorization: Bearer <token>
Content-Type: application/json
Accept: application/json
Accept-Encoding: gzip, deflate, br
If-None-Match: "abc123"          (conditional GET)
Idempotency-Key: <uuid>          (safe retries)
X-Request-ID: <uuid>             (tracing)

Response Headers

BASH
Content-Type: application/json; charset=utf-8
Cache-Control: max-age=3600, public
ETag: "abc123"
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1693000000
Retry-After: 60

CORS - Complete Explanation

CORS (Cross-Origin Resource Sharing) is enforced by browsers to prevent malicious sites from reading your API responses.

JAVASCRIPT
const cors = require('cors');

// Permissive (dev only - never in production)
app.use(cors());

// Production CORS
app.use(cors({
  origin: (origin, callback) => {
    const allowed = ['https://app.example.com', 'https://admin.example.com'];
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
  credentials: true,       // allows cookies/auth headers cross-origin
  maxAge: 86400,
}));

REST APIs

REST (Representational State Transfer) is an architectural style, not a protocol. Defined by Roy Fielding in 2000. It remains the most widely used API paradigm for public-facing APIs.

The 6 REST Constraints

1
Client-Server - Separation of concerns between UI and data
2
Stateless - Every request contains all info needed; no session on server
3
Cacheable - Responses must define themselves as cacheable or not
4
Uniform Interface - Consistent URLs, methods, representations
5
Layered System - Client can't tell if connected directly or via proxy
6
Code on Demand - Server can send executable code (optional)

Resource Design & URL Conventions

✅ Good URLs

BASH
GET  /users
POST /users
GET  /users/:id
PUT  /users/:id
DELETE /users/:id
GET  /users/:id/orders
GET  /users/:id/orders/:orderId
POST /users/:id/activate

❌ Bad URLs

BASH
GET  /getUsers
POST /createUser
GET  /user?id=123
POST /updateUser/123
GET  /deleteUser/123
GET  /getUserOrders?userId=123
POST /user/order/get
POST /doActivateUser

URL Design Rules

  • • Use nouns, not verbs in URLs
  • • Use plural for collections
  • • Use kebab-case for multi-word resources
  • Nest resources to show relationships (max 2-3 levels deep)
  • • Path params → identify a specific resource (/users/42)
  • • Query params → filter, sort, paginate (/users?role=admin&sort=name)

Versioning Strategies

JAVASCRIPT
// 1. URL Path (most common, most visible)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// GET /api/v2/users

// 2. Request Header
app.use((req, res, next) => {
  const version = req.headers['api-version'] || '1';
  req.apiVersion = version;
  next();
});

// 3. Accept Header (most RESTful)
// Accept: application/vnd.myapi.v2+json

// 4. Query Parameter (least recommended)
// GET /users?version=2

Pagination

JAVASCRIPTOffset-based (simple)
// GET /users?page=3&limit=20
const users = await db.query(
  'SELECT * FROM users LIMIT $1 OFFSET $2',
  [limit, (page - 1) * limit]
);
res.json({
  data: users,
  pagination: {
    page: 3, limit: 20, total: 1500,
    totalPages: 75, hasNext: true, hasPrev: true,
  }
});
JAVASCRIPTCursor-based (production)
// GET /users?cursor=eyJpZCI6MTAwfQ==&limit=20
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
const users = await db.query(
  'SELECT * FROM users WHERE id > $1 ORDER BY id ASC LIMIT $2',
  [decoded.id, limit]
);
const nextCursor = users.length
  ? Buffer.from(JSON.stringify({ id: users.at(-1).id })).toString('base64')
  : null;
res.json({ data: users, nextCursor });

Building a Production REST API

JAVASCRIPTsrc/routes/users.js
const express = require('express');
const { body, param, query, validationResult } = require('express-validator');
const router = express.Router();

// Middleware: validate & extract errors
const validate = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
  next();
};

// GET /users - list with pagination & filtering
router.get('/',
  query('page').optional().isInt({ min: 1 }).toInt(),
  query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
  query('role').optional().isIn(['admin', 'user', 'moderator']),
  validate,
  async (req, res, next) => {
    try {
      const { page = 1, limit = 20, role } = req.query;
      const users = await UserService.list({ page, limit, role });
      res.json(users);
    } catch (err) {
      next(err);
    }
  }
);

// POST /users
router.post('/',
  body('email').isEmail().normalizeEmail(),
  body('name').trim().isLength({ min: 2, max: 100 }),
  body('password').isStrongPassword(),
  validate,
  async (req, res, next) => {
    try {
      const user = await UserService.create(req.body);
      res.status(201).json(user);
    } catch (err) {
      if (err.code === 'DUPLICATE_EMAIL') {
        return res.status(409).json({ error: 'Email already exists' });
      }
      next(err);
    }
  }
);

module.exports = router;
JAVASCRIPTsrc/middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
  logger.error({ err, requestId: req.id, method: req.method, url: req.url });

  // Known operational errors
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      error: { message: err.message, code: err.code }
    });
  }

  // Unknown errors - don't leak details
  res.status(500).json({
    error: { message: 'Internal server error', code: 'INTERNAL_ERROR' }
  });
};

REST Best Practices Checklist

  • Always return consistent JSON error shapes
  • Use X-Request-ID header for distributed tracing
  • Set proper Cache-Control headers on GET responses
  • Validate all inputs before processing
  • Avoid returning arrays as root of response (use { data: [] })
  • Use 201 + Location header when creating resources
  • Never expose internal IDs (use UUIDs or opaque IDs)
  • Always support HEAD for your GET endpoints
  • Document with OpenAPI

GraphQL APIs

GraphQL is a query language for APIs developed by Facebook (2015). Clients request exactly the data they need - no over-fetching, no under-fetching.

Schema Definition Language (SDL)

GRAPHQL
type User {
  id: ID!
  name: String!
  email: String!
  orders: [Order!]!
  createdAt: DateTime!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  items: [OrderItem!]!
}

enum OrderStatus { PENDING PROCESSING SHIPPED DELIVERED CANCELLED }

type Query {
  user(id: ID!): User
  users(page: Int, limit: Int, role: String): UserConnection!
  me: User
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

type Subscription {
  orderStatusChanged(orderId: ID!): Order!
  newNotification: Notification!
}

Resolvers in Node.js (Apollo Server)

JAVASCRIPT
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      context.auth.requireRole('user');
      return context.dataSources.userDB.findById(id);
    },
    users: async (_, args, context) => {
      return context.dataSources.userDB.list(args);
    },
    me: async (_, __, context) => {
      return context.user; // from auth middleware
    },
  },

  Mutation: {
    createUser: async (_, { input }, context) => {
      context.auth.requireRole('admin');
      return context.dataSources.userDB.create(input);
    },
  },

  // Field-level resolver - runs for every User
  User: {
    orders: async (parent, _, context) => {
      // DataLoader batches to prevent N+1 queries
      return context.loaders.ordersByUserId.load(parent.id);
    },
  },

  Subscription: {
    orderStatusChanged: {
      subscribe: (_, { orderId }, context) => {
        return context.pubSub.asyncIterator(`ORDER_${orderId}`);
      },
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });

The N+1 Problem & DataLoader

The N+1 Problem

When resolving a list of users with their orders, GraphQL fires 1 query for users + N queries for each user's orders. DataLoader solves this by batching all order lookups into a single query.
JAVASCRIPT
// ❌ N+1 PROBLEM - fires a DB query for EVERY user
const resolvers = {
  User: {
    orders: (user) => db.orders.findByUserId(user.id),
  }
};

// ✅ SOLUTION - DataLoader batches & caches
const DataLoader = require('dataloader');

// Create once per request (in context factory)
const orderLoader = new DataLoader(async (userIds) => {
  // Single query for ALL user IDs
  const orders = await db.orders.findByUserIds(userIds);
  // Must return results in same order as input keys
  return userIds.map(id => orders.filter(o => o.userId === id));
});

// In resolver:
User: {
  orders: (user, _, context) => context.loaders.orders.load(user.id),
}

GraphQL Security

JAVASCRIPT
const { ApolloServer } = require('@apollo/server');
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(5),                          // prevent deeply nested queries
    createComplexityLimitRule(1000),        // prevent expensive queries
  ],
  // Disable introspection in production
  introspection: process.env.NODE_ENV !== 'production',
});

REST vs GraphQL - When to Use Which

  • Use REST when you have simple CRUD, public APIs, or need easy caching with HTTP semantics.
  • Use GraphQL when clients need flexible queries, you have deeply nested data, or you're building a BFF (Backend for Frontend).
  • Hybrid approach - many teams use REST for public APIs and GraphQL for internal/client-facing applications.

gRPC APIs

gRPC is a high-performance RPC framework by Google using Protocol Buffers. Ideal for microservice-to-microservice communication where low latency is critical.

Protocol Buffers (Protobuf)

PROTOBUFuser.proto
syntax = "proto3";
package user;

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);      // server streaming
  rpc CreateUsers (stream CreateUserRequest) returns (Summary); // client streaming
  rpc Chat (stream Message) returns (stream Message);          // bidirectional
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
  int64 created_at = 4;
}

message GetUserRequest {
  string id = 1;
}

message ListUsersRequest {
  int32 page = 1;
  int32 limit = 2;
}

gRPC Server in Node.js

JAVASCRIPT
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDef = protoLoader.loadSync('user.proto', {
  keepCase: true, longs: String, enums: String,
  defaults: true, oneofs: true,
});
const userProto = grpc.loadPackageDefinition(packageDef).user;

const server = new grpc.Server();

server.addService(userProto.UserService.service, {
  // Unary RPC
  getUser: async (call, callback) => {
    try {
      const user = await UserService.findById(call.request.id);
      if (!user) {
        return callback({
          code: grpc.status.NOT_FOUND,
          message: 'User not found',
        });
      }
      callback(null, user);
    } catch (err) {
      callback({ code: grpc.status.INTERNAL, message: err.message });
    }
  },

  // Server streaming RPC
  listUsers: async (call) => {
    const users = await UserService.list(call.request);
    for (const user of users) {
      call.write(user);
    }
    call.end();
  },
});

server.bindAsync('0.0.0.0:50051',
  grpc.ServerCredentials.createInsecure(),
  (err, port) => {
    if (err) throw err;
    console.log(`gRPC server running on port ${port}`);
  }
);

gRPC Client in Node.js

JAVASCRIPT
const client = new userProto.UserService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// Unary call
client.getUser({ id: 'abc123' }, (err, user) => {
  if (err) console.error(err);
  else console.log(user);
});

// Server streaming
const stream = client.listUsers({ page: 1, limit: 100 });
stream.on('data', (user) => console.log(user));
stream.on('end', () => console.log('Stream finished'));
stream.on('error', (err) => console.error(err));
FeatureRESTGraphQLgRPC
TransportHTTP/1.1HTTPHTTP/2
FormatJSONJSONProtobuf (binary)
StreamingLimitedSubscriptionsFull (4 types)
Best ForPublic APIsFlexible queriesMicroservices
Code GenerationOptional (OpenAPI)OptionalBuilt-in
Browser Support✅ Native✅ Native⚠️ gRPC-Web

SOAP APIs

SOAP (Simple Object Access Protocol) is an XML-based protocol common in enterprise/legacy systems - banking, ERP, and government.

SOAP Envelope Structure

XML
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <wsse:Security><!-- auth token --></wsse:Security>
  </soap:Header>
  <soap:Body>
    <GetUserRequest>
      <UserId>12345</UserId>
    </GetUserRequest>
  </soap:Body>
</soap:Envelope>

Consuming SOAP in Node.js

JAVASCRIPT
const soap = require('soap');

const WSDL_URL = 'http://www.dneonline.com/calculator.asmx?WSDL';

// Create SOAP client from WSDL
const client = await soap.createClientAsync(WSDL_URL);

// Call a SOAP method
const [result] = await client.AddAsync({ intA: 10, intB: 5 });
console.log(result.AddResult); // 15

// With authentication
client.addHttpHeader('Authorization', `Basic ${btoa('user:pass')}`);

SOAP vs REST - Legacy Integration

SOAP is still heavily used in banking, healthcare, and government APIs. You'll rarely build new SOAP services, but you'll often need to consume them from Node.js backends.

WebSockets

WebSockets provide full-duplex, persistent connections - both client and server can send messages at any time. Perfect for chat, gaming, and live collaboration.

WebSocket Handshake

BASH
Client → Server:
  GET /ws HTTP/1.1
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Server → Client:
  HTTP/1.1 101 Switching Protocols
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

# After handshake, HTTP is gone - raw WebSocket frames flow over TCP.

Raw WebSocket Server with ws

JAVASCRIPT
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');

const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map();

wss.on('connection', (ws, req) => {
  const clientId = uuidv4();
  clients.set(clientId, { ws, metadata: {} });

  ws.send(JSON.stringify({ type: 'CONNECTED', clientId }));

  // Heartbeat to detect dead connections
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });

  ws.on('message', (data) => {
    try {
      const msg = JSON.parse(data);
      handleMessage(clientId, msg);
    } catch (err) {
      ws.send(JSON.stringify({ type: 'ERROR', message: 'Invalid JSON' }));
    }
  });

  ws.on('close', (code) => {
    clients.delete(clientId);
    console.log(`Client ${clientId} disconnected: ${code}`);
  });
});

// Heartbeat interval
const heartbeat = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30_000);

function broadcast(data, excludeId = null) {
  const message = JSON.stringify(data);
  clients.forEach(({ ws }, id) => {
    if (id !== excludeId && ws.readyState === WebSocket.OPEN) {
      ws.send(message);
    }
  });
}

Socket.IO (Production Features)

JAVASCRIPT
const { Server } = require('socket.io');
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: { origin: 'https://app.example.com', credentials: true },
  pingTimeout: 60000,
  pingInterval: 25000,
});

// Authentication middleware
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.user = await verifyJWT(token);
    next();
  } catch (err) {
    next(new Error('Authentication failed'));
  }
});

io.on('connection', (socket) => {
  socket.join(`user:${socket.user.id}`);

  socket.on('join:room', (roomId) => {
    socket.join(`room:${roomId}`);
    socket.to(`room:${roomId}`).emit('user:joined', { userId: socket.user.id });
  });

  socket.on('message', (data) => {
    socket.to(`room:${data.roomId}`).emit('message', {
      ...data, userId: socket.user.id, timestamp: Date.now(),
    });
  });
});

// Send to specific user from anywhere
function notifyUser(userId, event, data) {
  io.to(`user:${userId}`).emit(event, data);
}

Scaling WebSockets with Redis Adapter

JAVASCRIPT
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
// Now events propagate across ALL Node.js processes

Server-Sent Events (SSE)

SSE provides one-way server → client streaming over a regular HTTP connection. Great for live dashboards, notifications, and AI text streaming.

FeatureSSEWebSocketLong Polling
DirectionServer → Client onlyBidirectionalServer → Client
ProtocolHTTPWebSocket (WS)HTTP
Auto-reconnect✅ Built-in❌ Manual❌ Manual
Load balancer✅ Friendly⚠️ Needs config✅ Friendly
Best forFeeds, notifications, AIChat, games, collabLegacy fallback
JAVASCRIPTSSE Implementation
app.get('/events', async (req, res) => {
  // Set SSE headers
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering
  res.flushHeaders();

  const sendEvent = (event, data, id) => {
    if (id) res.write(`id: ${id}\n`);
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  sendEvent('connected', { timestamp: Date.now() });

  const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
    sendEvent(event.type, event.data, event.id);
  });

  // Keepalive every 15s (prevents proxy timeouts)
  const keepalive = setInterval(() => {
    res.write(': keepalive\n\n');
  }, 15_000);

  req.on('close', () => {
    clearInterval(keepalive);
    unsubscribe();
  });
});

// Client-side JavaScript
const es = new EventSource('/events');
es.addEventListener('notification', (e) => {
  const data = JSON.parse(e.data);
  showNotification(data);
});

WebRTC

WebRTC enables peer-to-peer real-time communication (audio, video, data) directly between browsers.

BASH
Browser A ←── Signaling Server (your Node.js) ──→ Browser B
    │                                                   │
    └──────────────── Direct P2P Connection ────────────┘
                    (after ICE negotiation)

# The signaling server only helps browsers find each other
# - actual media flows P2P.

Node.js Signaling Server

JAVASCRIPT
const io = require('socket.io')(server);

io.on('connection', (socket) => {
  socket.on('join:room', (roomId) => {
    const room = io.sockets.adapter.rooms.get(roomId);
    const numClients = room ? room.size : 0;

    if (numClients >= 2) {
      socket.emit('room:full');
      return;
    }

    socket.join(roomId);
    socket.emit('room:joined', roomId);

    if (numClients === 1) {
      socket.emit('ready'); // Second peer triggers offer creation
    }
  });

  // Relay signaling messages between peers
  socket.on('offer', ({ roomId, offer }) => {
    socket.to(roomId).emit('offer', offer);
  });

  socket.on('answer', ({ roomId, answer }) => {
    socket.to(roomId).emit('answer', answer);
  });

  socket.on('ice-candidate', ({ roomId, candidate }) => {
    socket.to(roomId).emit('ice-candidate', candidate);
  });
});

Browser WebRTC Client

JAVASCRIPT
const peerConnection = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' },
    { urls: 'turn:your-turn-server.com', username: 'user', credential: 'pass' },
  ],
});

// Add local media tracks
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
stream.getTracks().forEach(track => peerConnection.addTrack(track, stream));

// Handle incoming remote stream
peerConnection.ontrack = (event) => {
  remoteVideo.srcObject = event.streams[0];
};

// Send ICE candidates through signaling
peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    socket.emit('ice-candidate', { roomId, candidate: event.candidate });
  }
};

// Create and send offer (Peer A)
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
socket.emit('offer', { roomId, offer });

When to Use What

  • WebSockets - bidirectional real-time (chat, games, live collaboration)
  • SSE - server push only (notifications, AI streaming, live feeds)
  • WebRTC - peer-to-peer (video calls, screen sharing, P2P data)

Webhooks

Webhooks are user-defined HTTP callbacks - your server calls someone else's server when an event occurs. The push model is far more efficient than polling.

Receiving Webhooks Securely

JAVASCRIPT
const express = require('express');
const crypto = require('crypto');

// CRITICAL: use raw body for signature verification
app.use('/webhooks', express.raw({ type: 'application/json' }));

function verifyStripeSignature(payload, sigHeader, secret) {
  const parts = sigHeader.split(',');
  const timestamp = parts.find(p => p.startsWith('t=')).split('=')[1];
  const sig = parts.find(p => p.startsWith('v1=')).split('=')[1];

  // Protect against replay attacks
  const tolerance = 300; // 5 minutes
  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > tolerance) {
    throw new Error('Webhook timestamp too old');
  }

  const signedPayload = `${timestamp}.${payload}`;
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Use timingSafeEqual to prevent timing attacks
  const expected = Buffer.from(expectedSig);
  const received = Buffer.from(sig);
  if (!crypto.timingSafeEqual(expected, received)) {
    throw new Error('Invalid webhook signature');
  }
}

app.post('/webhooks/stripe', async (req, res) => {
  try {
    verifyStripeSignature(req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).json({ error: err.message });
  }

  const event = JSON.parse(req.body);
  res.json({ received: true }); // Acknowledge IMMEDIATELY

  await webhookQueue.add(event.type, event); // Process async
});

Delivering Webhooks Reliably

JAVASCRIPT
const Queue = require('bull');
const axios = require('axios');

const webhookQueue = new Queue('webhooks', { redis: redisConfig });

webhookQueue.process(async (job) => {
  const { url, payload, secret, attempt } = job.data;

  const signature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  const response = await axios.post(url, payload, {
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': `sha256=${signature}`,
      'X-Webhook-ID': job.id,
      'X-Delivery-Attempt': attempt,
    },
    timeout: 10_000,
  });

  if (response.status >= 400) {
    throw new Error(`Webhook delivery failed: ${response.status}`);
  }
});

// Exponential backoff retry
webhookQueue.defaultJobOptions = {
  attempts: 5,
  backoff: { type: 'exponential', delay: 1000 }, // 1s, 2s, 4s, 8s, 16s
  removeOnComplete: 100,
  removeOnFail: 500,
};

tRPC

tRPC provides end-to-end type-safe APIs between Node.js backend and TypeScript frontend - no code generation, no schemas, just types that flow.

TYPESCRIPTserver/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.context<Context>().create();

export const appRouter = t.router({
  users: t.router({
    list: t.procedure
      .input(z.object({ page: z.number().min(1).default(1) }))
      .query(async ({ input, ctx }) => {
        return ctx.db.users.findMany({ skip: (input.page - 1) * 20 });
      }),

    create: t.procedure
      .input(z.object({ name: z.string().min(2), email: z.string().email() }))
      .mutation(async ({ input, ctx }) => {
        return ctx.db.users.create({ data: input });
      }),
  }),
});

// Client (React + TypeScript) - fully typed!
const { data } = trpc.users.list.useQuery({ page: 1 });
//       ^-- inferred: User[]

When to Use tRPC

  • • Full-stack TypeScript monorepo? → Use tRPC
  • • Public API consumed by third parties? → Use REST + OpenAPI
  • • Multiple client teams/languages? → Use GraphQL or REST

Message Queue APIs & Event-Driven Architecture

For decoupled, resilient async communication between services. When you need to guarantee processing even if consumers are temporarily down.

BullMQ (Redis-based Queue)

JAVASCRIPT
const { Queue, Worker } = require('bullmq');
const connection = { host: 'localhost', port: 6379 };

// Producer
const emailQueue = new Queue('emails', { connection });

await emailQueue.add('welcome-email', {
  to: '[email protected]',
  templateId: 'welcome',
  userId: '123',
}, {
  delay: 5000,        // send after 5 seconds
  attempts: 3,
  backoff: { type: 'exponential', delay: 2000 },
  removeOnComplete: { age: 3600, count: 1000 },
  removeOnFail: { age: 24 * 3600 },
});

// Consumer
const worker = new Worker('emails', async (job) => {
  const { to, templateId, userId } = job.data;
  await emailService.send(to, templateId, userId);
  return { sent: true };
}, { connection, concurrency: 10 });

worker.on('completed', (job) => console.log(`Job ${job.id} completed`));
worker.on('failed', (job, err) => console.error(`Job ${job.id} failed:`, err));

Kafka with Node.js

JAVASCRIPT
const { Kafka } = require('kafkajs');

const kafka = new Kafka({ brokers: ['kafka:9092'] });

// Producer
const producer = kafka.producer();
await producer.connect();

await producer.send({
  topic: 'user-events',
  messages: [{
    key: userId,
    value: JSON.stringify({ type: 'USER_CREATED', userId, timestamp: Date.now() }),
    headers: { 'content-type': 'application/json' },
  }],
});

// Consumer
const consumer = kafka.consumer({ groupId: 'notification-service' });
await consumer.connect();
await consumer.subscribe({ topics: ['user-events'], fromBeginning: false });

await consumer.run({
  eachMessage: async ({ topic, partition, message }) => {
    const event = JSON.parse(message.value.toString());
    await handleEvent(event);
  },
});

API Security

JWT - Production Implementation

JAVASCRIPT
const jwt = require('jsonwebtoken');

// Access token: short-lived (15 minutes)
function generateAccessToken(user) {
  return jwt.sign(
    { sub: user.id, email: user.email, role: user.role },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: '15m', issuer: 'api.example.com', audience: 'app.example.com' }
  );
}

// Refresh token: long-lived (7 days), stored in DB
function generateRefreshToken(user) {
  return jwt.sign(
    { sub: user.id, tokenFamily: uuidv4() },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
}

// Middleware
const authenticate = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing token' });
  }
  const token = authHeader.slice(7);
  try {
    const payload = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, {
      issuer: 'api.example.com',
      audience: 'app.example.com',
    });
    req.user = payload;
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
};

Rate Limiting

JAVASCRIPT
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Global rate limit
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 1000,
  standardHeaders: true,
  legacyHeaders: false,
  store: new RedisStore({ client: redisClient }),
}));

// Strict limit for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  message: { error: 'Too many login attempts', retryAfter: 900 },
  keyGenerator: (req) => `${req.ip}:${req.body?.email ?? 'unknown'}`,
});

app.post('/auth/login', authLimiter, loginHandler);

OWASP API Security Top 10 (2023)

1
Broken Object Level Authorization - Always check user owns the resource
2
Broken Authentication - Proper token validation, no secrets in URLs
3
Broken Object Property Level Authorization - Don't return excess fields
4
Unrestricted Resource Consumption - Rate limiting, pagination limits
5
Broken Function Level Authorization - Check permissions per endpoint
6
Unrestricted Access to Sensitive Business Flows - Anti-automation
7
Server-Side Request Forgery (SSRF) - Validate URLs in user input
8
Security Misconfiguration - Remove default creds, disable debug in prod
9
Improper Inventory Management - Document and retire old API versions
10
Unsafe Consumption of APIs - Validate third-party API responses

API Performance & Optimization

Caching Strategy (Redis)

JAVASCRIPT
const redis = require('ioredis');
const client = new redis(process.env.REDIS_URL);

// Cache middleware
const cache = (ttl = 60) => async (req, res, next) => {
  if (req.method !== 'GET') return next();

  const key = `cache:${req.originalUrl}:${req.user?.id ?? 'anon'}`;
  const cached = await client.get(key);

  if (cached) {
    res.setHeader('X-Cache', 'HIT');
    return res.json(JSON.parse(cached));
  }

  const originalJson = res.json.bind(res);
  res.json = (data) => {
    client.setex(key, ttl, JSON.stringify(data)).catch(() => {});
    res.setHeader('X-Cache', 'MISS');
    return originalJson(data);
  };

  next();
};

// Use on specific routes
app.get('/products', cache(300), getProducts);     // 5 min
app.get('/products/:id', cache(3600), getProduct); // 1 hour

HTTP Caching with ETags

JAVASCRIPT
const crypto = require('crypto');

app.get('/users/:id', async (req, res) => {
  const user = await UserService.findById(req.params.id);
  if (!user) return res.status(404).end();

  const etag = `"${crypto.createHash('md5').update(JSON.stringify(user)).digest('hex')}"`;
  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');

  // Conditional GET - avoid sending body if unchanged
  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.json(user);
});

Compression

JAVASCRIPT
const compression = require('compression');

app.use(compression({
  filter: (req, res) => {
    if (req.headers['x-no-compression']) return false;
    return compression.filter(req, res);
  },
  level: 6,      // balance between speed and compression
  threshold: 1024, // only compress responses > 1KB
}));

Performance Quick Wins

  • • Use Redis for hot-path caching (user sessions, product listings)
  • • Set ETags on GET responses for conditional requests
  • • Enable gzip/Brotli compression for all JSON responses
  • • Use connection pooling for database and HTTP agents
  • • Stream large responses instead of buffering in memory

API Design Patterns

Service Layer Pattern

BASH
Routes → Controllers → Services → Repositories → Database
           ↓               ↓
       Validates        Business
       HTTP layer        Logic
JAVASCRIPT
// Repository - only DB queries
class UserRepository {
  async findById(id) { return db.query('SELECT * FROM users WHERE id = $1', [id]); }
  async create(data) { return db.query('INSERT INTO users...', [...Object.values(data)]); }
}

// Service - business logic
class UserService {
  constructor(userRepo, emailService) {
    this.userRepo = userRepo;
    this.emailService = emailService;
  }

  async create(data) {
    const existing = await this.userRepo.findByEmail(data.email);
    if (existing) throw new ConflictError('Email already registered');

    const hashed = await bcrypt.hash(data.password, 12);
    const user = await this.userRepo.create({ ...data, password: hashed });

    await this.emailService.sendWelcome(user.email);
    return this.sanitize(user);
  }

  sanitize(user) {
    const { password, ...safe } = user;
    return safe;
  }
}

// Controller - HTTP concern only
const createUser = async (req, res, next) => {
  try {
    const user = await userService.create(req.body);
    res.status(201)
       .setHeader('Location', `/users/${user.id}`)
       .json(user);
  } catch (err) {
    next(err);
  }
};

Long-Running Jobs (202 Accepted)

JAVASCRIPT
// POST /exports - start async job
app.post('/exports', authenticate, async (req, res) => {
  const jobId = uuidv4();

  // Start async job (don't await)
  exportQueue.add(jobId, { userId: req.user.id, params: req.body });

  res.status(202)
     .setHeader('Location', `/exports/${jobId}/status`)
     .json({ jobId, status: 'queued' });
});

// GET /exports/:jobId/status - poll for status
app.get('/exports/:jobId/status', authenticate, async (req, res) => {
  const job = await exportQueue.getJob(req.params.jobId);
  const state = await job.getState();

  const statusMap = { waiting: 'queued', active: 'processing',
                      completed: 'done', failed: 'failed' };

  res.json({
    jobId: job.id,
    status: statusMap[state],
    progress: job.progress,
    result: state === 'completed' ? job.returnvalue : null,
    error: state === 'failed' ? job.failedReason : null,
  });
});

API Gateway & Reverse Proxy

Nginx as API Gateway

NGINX
upstream api_v1 {
    least_conn;
    server app1:3000 weight=5;
    server app2:3000 weight=5;
    keepalive 32;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # SSL
    ssl_certificate /etc/ssl/certs/api.crt;
    ssl_certificate_key /etc/ssl/private/api.key;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m;
    limit_req zone=api burst=20 nodelay;

    location /api/ {
        proxy_pass http://api_v1;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Request-ID $request_id;

        # Timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

Microservices & API Communication

Circuit Breaker Pattern

Prevents cascading failures when a downstream service goes down. The circuit opens after too many failures and stops sending requests until the service recovers.

JAVASCRIPT
const CircuitBreaker = require('opossum');

const paymentServiceCall = (data) => axios.post('http://payment-service/charge', data);

const breaker = new CircuitBreaker(paymentServiceCall, {
  timeout: 3000,        // fail if takes > 3s
  errorThresholdPercentage: 50,  // open if >50% fail
  resetTimeout: 30000,  // try again after 30s
  volumeThreshold: 10,  // min requests before calculating %
});

breaker.on('open', () => logger.warn('Payment circuit breaker OPEN'));
breaker.on('halfOpen', () => logger.info('Payment circuit breaker testing'));
breaker.on('close', () => logger.info('Payment circuit breaker CLOSED'));

// Fallback when circuit is open
breaker.fallback((data) => ({ status: 'queued', message: 'Processing delayed' }));

// Use
const result = await breaker.fire(paymentData);

Microservices Communication Rules

  • Sync (HTTP/gRPC) - only for request-response where you need an immediate answer
  • Async (Message Queue) - for fire-and-forget events, decoupled processing
  • • Always implement circuit breakers for inter-service calls
  • • Use service discovery instead of hardcoded URLs
  • • Implement the Saga pattern for distributed transactions

API Testing

Integration Testing with Supertest

JAVASCRIPT
const request = require('supertest');
const app = require('../src/app');

describe('POST /users', () => {
  it('creates a user and returns 201', async () => {
    const res = await request(app)
      .post('/api/v1/users')
      .set('Authorization', `Bearer ${testToken}`)
      .send({ name: 'Alice', email: '[email protected]', password: 'Str0ng!Pass' });

    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({ name: 'Alice', email: '[email protected]' });
    expect(res.body).not.toHaveProperty('password');
    expect(res.headers.location).toMatch(/\/users\/.+/);
  });

  it('returns 409 for duplicate email', async () => {
    await createUser({ email: '[email protected]' });
    const res = await request(app)
      .post('/api/v1/users')
      .send({ email: '[email protected]', name: 'Bob', password: 'Str0ng!Pass' });

    expect(res.status).toBe(409);
    expect(res.body.error).toBeDefined();
  });
});

Load Testing with k6

JAVASCRIPTk6 load test script
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const errorRate = new Rate('errors');

export const options = {
  stages: [
    { duration: '1m', target: 50 },   // ramp up to 50 users
    { duration: '3m', target: 50 },   // hold
    { duration: '1m', target: 200 },  // spike
    { duration: '2m', target: 200 },  // hold spike
    { duration: '1m', target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
    errors: ['rate<0.01'],
  },
};

export default function() {
  const res = http.get(`${__ENV.API_URL}/users`, {
    headers: { Authorization: `Bearer ${__ENV.TOKEN}` },
  });

  const success = check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  errorRate.add(!success);
  sleep(1);
}

API Documentation

OpenAPI 3.x Specification

YAML
openapi: 3.1.0
info:
  title: My Production API
  version: 2.0.0
  description: Production-grade RESTful API

servers:
  - url: https://api.example.com/v2
    description: Production
  - url: https://staging-api.example.com/v2
    description: Staging

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    User:
      type: object
      required: [id, name, email, createdAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 2 }
        email: { type: string, format: email }
        createdAt: { type: string, format: date-time }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [message, code]
          properties:
            message: { type: string }
            code: { type: string }

security:
  - bearerAuth: []

paths:
  /users:
    get:
      summary: List users
      operationId: listUsers
      parameters:
        - name: page
          in: query
          schema: { type: integer, minimum: 1, default: 1 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        '200':
          description: Paginated user list

DX Best Practices

  • • Use Swagger UI, Redoc, or Scalar for interactive docs
  • • Maintain a changelog for every API version
  • • Provide runnable code samples in multiple languages
  • • Include a Postman collection for quick testing

API Observability & Monitoring

Structured Logging with Pino

JAVASCRIPT
const pino = require('pino');

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),
  },
  base: {
    service: 'user-api',
    version: process.env.npm_package_version,
    env: process.env.NODE_ENV,
  },
});

// Request logging middleware
app.use((req, res, next) => {
  req.log = logger.child({ requestId: req.id });
  const start = Date.now();

  res.on('finish', () => {
    req.log.info({
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      duration: Date.now() - start,
    }, 'request completed');
  });

  next();
});

OpenTelemetry Tracing

JAVASCRIPT
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http');
const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express');
const { PgInstrumentation } = require('@opentelemetry/instrumentation-pg');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4318/v1/traces' }),
  instrumentations: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
    new PgInstrumentation(),
  ],
  serviceName: 'user-api',
});

sdk.start(); // Must be called before any other imports!

Health Check Endpoint

JAVASCRIPT
app.get('/health', async (req, res) => {
  const checks = await Promise.allSettled([
    db.query('SELECT 1'),
    redis.ping(),
    checkExternalService(),
  ]);

  const [db_check, redis_check, external_check] = checks;
  const allHealthy = checks.every(c => c.status === 'fulfilled');

  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? 'healthy' : 'degraded',
    timestamp: new Date().toISOString(),
    version: process.env.npm_package_version,
    checks: {
      database: db_check.status === 'fulfilled' ? 'up' : 'down',
      cache: redis_check.status === 'fulfilled' ? 'up' : 'down',
      external: external_check.status === 'fulfilled' ? 'up' : 'down',
    },
    uptime: process.uptime(),
  });
});

Deployment & CI/CD for APIs

Production Dockerfile

DOCKERFILE
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache dumb-init

# Dependencies stage
FROM base AS deps
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Build stage (if using TypeScript)
FROM base AS build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM base AS production
ENV NODE_ENV=production

# Run as non-root
USER node

COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json .

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget --quiet --tries=1 http://localhost:3000/health || exit 1

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

Graceful Shutdown

JAVASCRIPT
const server = app.listen(PORT, () => {
  logger.info(`Server listening on port ${PORT}`);
});

// Track active connections
let connections = new Set();
server.on('connection', (conn) => {
  connections.add(conn);
  conn.on('close', () => connections.delete(conn));
});

const gracefulShutdown = async (signal) => {
  logger.info(`Received ${signal}, shutting down gracefully`);

  // Stop accepting new connections
  server.close(async () => {
    logger.info('HTTP server closed');

    // Cleanup resources
    await Promise.all([
      db.end(),
      redis.quit(),
      worker.close(),
    ]);

    logger.info('All connections closed, exiting');
    process.exit(0);
  });

  // Force-close lingering connections after 30s
  setTimeout(() => {
    logger.warn('Forcing close of remaining connections');
    connections.forEach((conn) => conn.destroy());
  }, 30_000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT',  () => gracefulShutdown('SIGINT'));

Real-World Production Patterns

File Upload with Presigned URLs (AWS S3)

JAVASCRIPT
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3 = new S3Client({ region: 'us-east-1' });

app.post('/uploads/presigned', authenticate, async (req, res) => {
  const { filename, contentType } = req.body;

  const allowed = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (!allowed.includes(contentType)) {
    return res.status(422).json({ error: 'File type not allowed' });
  }

  const key = `uploads/${req.user.id}/${uuidv4()}-${filename}`;

  const url = await getSignedUrl(s3, new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    ContentType: contentType,
    ContentLength: req.body.fileSize,
    Metadata: { userId: req.user.id },
  }), { expiresIn: 300 }); // 5 minutes

  res.json({ uploadUrl: url, key, expiresIn: 300 });
});

Idempotency Keys

JAVASCRIPT
const idempotency = async (req, res, next) => {
  const key = req.headers['idempotency-key'];
  if (!key) return next();

  const cacheKey = `idempotency:${req.user.id}:${key}`;
  const cached = await redis.get(cacheKey);

  if (cached) {
    const { statusCode, body } = JSON.parse(cached);
    res.setHeader('X-Idempotent-Replayed', 'true');
    return res.status(statusCode).json(body);
  }

  const originalJson = res.json.bind(res);
  res.json = (data) => {
    redis.setex(cacheKey, 86400, JSON.stringify({
      statusCode: res.statusCode,
      body: data,
    }));
    return originalJson(data);
  };

  next();
};

app.post('/payments', authenticate, idempotency, processPayment);

API Versioning & Sunset Headers

JAVASCRIPT
// Sunset header - warn clients about deprecation
app.use('/api/v1', (req, res, next) => {
  res.setHeader('Deprecation', 'true');
  res.setHeader('Sunset', 'Sat, 31 Dec 2025 23:59:59 GMT');
  res.setHeader('Link', '</api/v2/docs>; rel="successor-version"');
  next();
}, v1Router);

Comparing All API Paradigms

ParadigmTransportDirectionFormatBest Use CaseNode.js Library
RESTHTTPRequest/ResponseJSONCRUD APIs, public APIsExpress, Fastify
GraphQLHTTPRequest/ResponseJSONFlexible queries, BFFApollo, Yoga
gRPCHTTP/2All stream typesProtobufMicroservices, high perf@grpc/grpc-js
SOAPHTTPRequest/ResponseXMLEnterprise/legacynode-soap
WebSocketTCPBidirectionalAnyReal-time, chat, gamesws, Socket.IO
SSEHTTPServer→ClientTextLive feeds, AI streamingNative (res.write)
WebhookHTTPServer→ServerJSONEvent notificationsExpress receiver
WebRTCUDPP2PBinaryVideo/audio, P2P dataSignaling server
tRPCHTTPRequest/ResponseJSONFull-stack TypeScript@trpc/server
Message QueueTCPAsyncAnyDecoupled servicesBullMQ, kafkajs

Decision Guide

Need a public API for web/mobile?→ Use REST
Frontend needs very specific data shapes / aggregation?→ Use GraphQL
Internal microservice-to-microservice (high throughput)?→ Use gRPC
Need real-time bidirectional communication (chat, multiplayer)?→ Use WebSockets
Need to push server updates to browser (notifications, AI)?→ Use SSE
Need to notify another system when something happens?→ Use Webhooks
Need peer-to-peer audio/video?→ Use WebRTC
Full-stack TypeScript, need type safety?→ Use tRPC
Need decoupled async processing across services?→ Use Message Queue
Must integrate with bank/ERP/government legacy?→ Use SOAP

You're Now an API Master! 🚀

You've covered the full API landscape for Node.js - from HTTP fundamentals to production deployment. Work through the code examples, implement them in your projects, and architect any API system with confidence.

Recommended Resources
How To Practice Coding Every Day
Han Shavir

Build a Consistent Coding Habit

Stop guessing and start building. This e-book provides practical strategies, exercises, and routines to help you code regularly and improve steadily.

Get E-Book
How to Read and Understand Other People's Code
Han Shavir

Master Unfamiliar Codebases

Struggling to make sense of someone else's code? Learn practical strategies to navigate, analyze, and master unfamiliar codebases with confidence.

Get E-Book

Tags

#Node.js#REST API#GraphQL#gRPC#WebSockets#Webhooks#SSE#tRPC#API Security#JWT#API Design Patterns#Microservices#Express.js#Docker#API Testing#OpenAPI
Dev Kant Kumar

Dev Kant Kumar

Author

Full Stack Developer passionate about crafting high-performance user experiences. I write about Agentic AI, React, and the future of web development.

💬 Discussion

Recommended Resources
How To Practice Coding Every Day
Han Shavir

Build a Consistent Coding Habit

Stop guessing and start building. This e-book provides practical strategies, exercises, and routines to help you code regularly and improve steadily.

Get E-Book
How to Read and Understand Other People's Code
Han Shavir

Master Unfamiliar Codebases

Struggling to make sense of someone else's code? Learn practical strategies to navigate, analyze, and master unfamiliar codebases with confidence.

Get E-Book