npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

uninet

v0.1.2

Published

HTTP, WebSocket, and SSE server for Node.js — server only; use uninet-client for client (no dependency)

Readme

uninet

HTTP, WebSocket, and SSE server for Node.js. Server only; no client code. Use uninet-client for the client (separate package, no dependency between them).

Packages

| Package | Purpose | |--------|---------| | uninet (this) | Server only: createServer, HTTP, WS, SSE, middleware, upload, CORS, cookies, body validator. | | uninet-client | Client only: fetch, createWebSocket, createUninetClient. No dependency on uninet. |

npm install uninet          # server
npm install uninet-client   # client (in app or another repo)

Positioning

  • Simpler than Express — no view engine, one middleware model.
  • Faster than Express — trie router, linear middleware, minimal allocations.
  • Lighter than Socket.IO — optional ws only when you enable WebSocket.
  • Server + client split — uninet (server) and uninet-client (client) are separate packages with no dependency.

Comparison & benchmarks: COMPARISON.md — SDK/client compatibility (Socket.IO–style), features vs Express/Socket.IO/Fastify/Hono, performance & throughput.

Platform support: Server = Node.js (Windows, macOS, Linux). Client = Android, iOS, Windows, macOS, Linux — browser, Node, or React Native / JS runtimes (same API; see COMPARISON.md#11-platform-support).

Docs & examples:

Install

npm install uninet

Optional dependencies:

  • WebSocket server (ws: true): npm install ws
  • Body validator with Zod: npm install zod

Engine: Default is uWS for higher throughput. Use createServer({ engine: "node" }) to use Node's built-in HTTP server (e.g. for compatibility or testing).

API

Server

import { createServer, bodyParser, cookieParser, cors } from "uninet";

const app = createServer({
  port: 3000,
  host: "0.0.0.0",
  ws: true,
  sse: true,
  timeout: 30_000,
  pingInterval: 25_000,   // heartbeat (ms); 0 = disabled
  recoveryBufferSize: 100, // events stored for reconnection recovery
});

app.use(cors({ origin: "*", credentials: true }));
app.use(cookieParser("optional-secret")); // req.cookies, res.cookie(), res.clearCookie()
app.use(bodyParser()); // body already parsed (JSON, urlencoded, text)
app.use(async (req, res, next) => {
  console.log(req.method, req.path);
  await next();
});

app.get("/users", (req, res) => {
  res.json({ users: [] });
});

app.post("/users", (req, res) => {
  const body = req.body as { name: string };
  res.status(201).json({ id: "1", ...body });
});

app.ws("/chat", {
  open(ws, req, io) {
    ws.emit("welcome", { id: ws.id });
    ws.join("lobby");
  },
  message(ws, data, io) {
    io.broadcast("message", { text: data.toString(), from: ws.id });
  },
  close(ws) {},
  error(ws, err) {},
});
// From any HTTP controller: broadcast / send to particular socket or room
const io = app.getWsNamespace?.("/chat");
io?.broadcast("event", payload);
io?.to(socketId).emit("dm", payload);
io?.to(roomId).emit("room", payload);

app.sse("/events", (stream, req) => {
  stream.send({ type: "ping" });
});

await app.listen();
// await app.close();

HTTP handler

  • RequestContext: method, url, path, headers, query, params, body, cookies (with cookieParser), raw (IncomingMessage).
  • ResponseContext: status(code), setHeader(name, value), json(data), sendPrepared(body, contentType?), text(data), redirect(url), cookie(name, value, options?), clearCookie(name), error(err), end(). For hot paths, prefer res.sendPrepared(Buffer.from('{"ok":1}', "utf8")) to skip JSON.stringify and avoid extra allocations.

Middleware

(req: RequestContext, res: ResponseContext, next: () => Promise<void>) => void | Promise<void>

Linear execution; call next() to continue.

Body validator (Zod-style)

Validate req.body with any schema that has .parse(data). Works with Zod (optional: npm i zod).

import { createServer, bodyParser, bodyValidator } from "uninet";
import { z } from "zod";

const app = createServer({ port: 3000 });
app.use(bodyParser());

const createUserSchema = z.object({ name: z.string().min(1), email: z.string().email() });
app.post("/users", (req, res) => {
  bodyValidator(createUserSchema)(req, res, () => {
    const body = req.body; // typed & validated
    res.status(201).json({ id: "1", ...body });
  });
});

On validation failure: 400 with { message: "Validation failed", errors: [...] } (Zod issues when available).

Middleware order (recommended)

cors → bodyParser → createUpload → auth → rateLimit → accessLog → securityHeaders → routes

Add-ons (middleware & helpers)

  • rateLimit — Limit requests per key (IP or custom) per window: app.use(rateLimit({ windowMs: 60_000, max: 100 })). Use ignorePaths: ["/chat/poll"] to skip high-frequency paths.
  • accessLog — Log method, path, status, duration: app.use(accessLog({ format: "short", ignorePaths: ["/chat/poll"] })).
  • securityHeaders — Set X-Content-Type-Options, X-Frame-Options, optional HSTS: app.use(securityHeaders({ hsts: false })).
  • requestId — Add X-Request-Id to request/response (or use client-provided): app.use(requestId()). Sets req.requestId.
  • structuredLog — Span context per request: req.span / res.span with traceId and spanId; JSON log on finish: app.use(structuredLog({ injectResponseHeaders: true })). Optional X-Trace-Id header propagation.
  • health — Health check handler for K8s/LB: app.get("/health", health({ ready: () => db.isConnected() })). Returns 200/503.
  • gracefulShutdown — Listen for SIGTERM/SIGINT and call app.close(): gracefulShutdown(app, { onClosed: () => process.exit(0) }).
  • createStatic — Static file serving: app.use(createStatic({ routePrefix: '/public', directory: path.join(__dirname, 'public'), etag: true, spa: true })). Use spa: true for SPA fallback (serve index.html when file not found).
  • res.download(filePath, filename?) — Send file as attachment.
  • req.context — Use req.context = { userId } in middleware; pass data to handlers.
  • compress — Gzip response body when Accept-Encoding: gzip and size > threshold: app.use(compress({ threshold: 1024 })).
  • WS auth — Reject WebSocket upgrade with 401 if auth fails: app.ws('/chat', { auth: (req) => validateToken(req), open(...), message(...) }).
  • wsCompression — Enable WebSocket per-message deflate: createServer({ ws: true, wsCompression: true }).
  • getStickyKey — Sticky session key for load balancers: getStickyKey(req, 'x-session-id'); use with LB so same client hits same instance for WS.
  • Streaming responseres.stream(async function* () { yield chunk; }) (Transfer-Encoding: chunked).
  • Request stream — Use streamBody: true in route options so body is not read; use req.bodyStream (same as req.raw) for streaming uploads: app.post('/upload', handler, { streamBody: true }).
  • Schema at route — Validate body/query before handler: app.post('/users', handler, { body: userSchema, query: paginationSchema }). 400 with structured errors on failure.
  • SSE middleware — Global app.use() middlewares run before SSE handlers; auth, rate limit, requestId, etc. apply to SSE routes. After middleware, the SSE stream is created and passed to the handler.

HTTP client (uninet-client)

Use uninet-client for fetch (axios-like):

import { fetch } from "uninet-client";

const res = await fetch("https://api.example.com", {
  method: "POST",
  json: { hello: "world" },
  timeout: 5000,
});
const data = await res.json();

Supports: method, headers, body, json, timeout, signal (AbortSignal).

Socket.IO-style WebSocket (io, broadcast, rooms)

  • io (WsNamespace): passed to open(ws, req, io) and message(ws, data, io). Use from anywhere via app.getWsNamespace(path) (e.g. in HTTP controllers).
  • io.broadcast(event, data) — send to all connected sockets in this path.
  • io.to(socketId).emit(event, data) — send to one socket (by ws.id).
  • io.to(roomId).emit(event, data) — send to all sockets in a room.
  • ws.id — unique socket id.
  • ws.emit(event, data) — send to this socket only (client receives JSON { type, data }).
  • ws.join(roomId) / ws.leave(roomId) / ws.rooms() — room membership.
  • Acknowledgements: io.to(id).emit("event", data, { ack: (err, data) => {} }) — callback when client sends _ack.
  • Volatile: io.broadcast("event", data, { volatile: true }) — not stored for reconnection recovery.
  • Connection state recovery: server stores last N events per namespace; client sends lastEventId on reconnect and receives missed events.
  • Heartbeat: pingInterval in options sends ping/pong; disconnect if no pong.
  • HTTP fallback: GET path/poll?lastEventId= and POST path/emit for clients that fall back from WebSocket (unified client uses these).
  • Distributed WebSocket (Redis) — Optional adapter to sync broadcast/rooms across instances: app.ws('/chat', handlers, { adapter: createRedisAdapter(redisClient) }). Use package uninet-adapter-redis (npm i uninet-adapter-redis redis). Same API; messages are published to Redis and delivered to local sockets on every instance.

Client (separate package: uninet-client)

For fetch, createWebSocket, and createUninetClient (WS-first, HTTP fallback, auto-reconnect, cookies), use the uninet-client package. No dependency on uninet.

npm install uninet-client
import { createUninetClient, createWebSocket, fetch } from "uninet-client";
import { createUninetClient } from "uninet";

const client = createUninetClient("http://localhost:3001", {
  path: "/chat",
  transports: ["ws", "http"],
  reconnection: true,
  reconnectionAttempts: Infinity,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  withCredentials: true, // send/store cookies
  debug: false,
});

client.on("connect", () => {});
client.on("disconnect", () => {});
client.on("message", (data) => {});
client.on("eventName", (data) => {});

client.emit("eventName", payload, (err, data) => { /* ack */ });
client.connect();
// client.disconnect();
  • Auto-reconnect with exponential backoff.
  • Cookies: withCredentials: true uses a cookie jar (send Cookie, store Set-Cookie).
  • Acknowledgements: pass a callback as third arg to emit; server can reply with _ack.

Built-in upload (dependency-free, HTTP + WS)

No multer; pure Node (Buffer + fs). Prefer WS for file upload (chunked over WebSocket), fallback HTTP multipart.

HTTP multipart — single / multiple / multi-field:

import { createServer, createUpload } from "uninet";

const app = createServer({ port: 3000 });

app.use(
  createUpload({
    prefix: "file",
    directory: "uploads",
    sizeLimit: 5 * 1024 * 1024,
    fieldName: "file",
    multiple: true,
    maxCount: 3,
    allowedTypes: ["image", "pdf", "doc", "video"],
    // fields: [{ name: "avatar", maxCount: 1 }, { name: "gallery", maxCount: 5 }],
  })
);

app.post("/upload", (req, res) => {
  const urls = req.files; // string[] or Record<string, string[]>
  const body = req.body;   // non-file fields
  res.json({ success: true, files: urls });
});

WS file upload (prefer for throughput) — client sends file:start, file:chunk (base64), file:end; server replies file:done with URL:

import { createWsFileReceiver } from "uninet";

const receiveFile = createWsFileReceiver({ directory: "uploads", prefix: "wsfile" });

app.ws("/upload", {
  message(ws, data, io) {
    try {
      const p = JSON.parse(data.toString());
      receiveFile(ws, p);
    } catch {}
  },
});

WebSocket client (raw)

import { createWebSocket } from "uninet";

const ws = createWebSocket("wss://example.com", {
  open() {},
  message(data) {},
  close() {},
  error(err) {},
});

Embedded mode

Pass an existing Node http.Server:

import * as http from "node:http";
import { createServer } from "uninet";

const server = http.createServer();
const app = createServer({ server, ws: true });
app.get("/health", (req, res) => res.json({ ok: true }));
await app.listen();
server.listen(3000);

Types

All public types are exported from uninet: RequestContext, ResponseContext, Middleware, HttpHandler, WsHandlers, SseStream, CreateServerOptions, App, FetchOptions, FetchResponse, etc.

Error handling

  • createError(statusCode, message, code?) — create typed HTTP errors.
  • res.error(err) — send JSON error (no stack in production).
  • HttpError, isHttpError, formatError — for custom handling.

Dependency map

| Feature | Dependency | When | |----------------|-------------|--------------------------| | HTTP server | Node http | Always | | SSE | None | Always (built-in) | | WebSocket server | ws (optional) | When ws: true | | WebSocket client | Global WebSocket or ws | When using createWebSocket | | HTTP client | Global fetch (Node 18+) | When using fetch |

Performance notes

  • Trie-based router: O(path segments), no regex.
  • Body read once per request; parsed as JSON or text.
  • Middleware runs in a single linear chain; no recursion.
  • SSE uses raw ServerResponse.write; no extra buffering.
  • Optional ws is lazy-loaded when the first upgrade is handled.
  • Hot paths: Use res.sendPrepared(Buffer.from('{"ok":1}', "utf8")) for static/semi-static JSON to skip JSON.stringify.

Author

ArunLinkedIn · GitHub · pluskode.com

License

MIT