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

@dooboostore/simple-boot-http-server

v1.0.31

Published

back end http server frameworks

Readme

@dooboostore/simple-boot-http-server

NPM version Build and Test License: MIT

@dooboostore/simple-boot-http-server is a lightweight, high-performance HTTP web server framework for Node.js, built on top of @dooboostore/simple-boot. It provides a decorator-driven, declarative programming model that allows you to design complex backend architectures intuitively and robustly.


🚀 Key Features

  • Declarative Routing: Map URL paths and HTTP methods directly to your controller methods using decorators like @Router, @Route, @GET, @POST, @PUT, @DELETE, @PATCH.
  • Dependency Injection (DI): Manage dependencies between services automatically using the @Sim decorator — the exact same DI/AOP container as @dooboostore/simple-boot.
  • Middleware & Filters: Systematically separate pre/post-processing logic (CORS, caching, auth, logging, OpenAPI generation, etc.) through the Filter interface.
  • Powerful Exception Handling: Leverage AOP-based GlobalAdvice to isolate business logic from error handling logic.
  • Generator Streaming: Return a (async) generator and each yielded value is written to the body immediately after the headers — SSE with no plumbing.
  • Content-Type Streams: ContentTypeGenerator adds the wire format for SSE, MJPEG/multipart, NDJSON, JSON Text Sequences, streamed JSON arrays, CSV, and text lines — pull from your generator or push() into a channel.
  • Manual Response Streaming: Opt out of the automatic response pipeline with res: 'manual' to stream Server-Sent Events, NDJSON, or any long-lived response directly from the injected ServerResponse.
  • Real-time WebSocket Protocol: A topic-based pub/sub protocol over a single socket — subscribe/unsubscribe, request/response, and server-initiated events the server can await a client reply for — with automatic reconnection and re-subscription.
  • Binary File Transfer over WebSocket: Buffer/File values anywhere in a message are detected automatically and shipped as raw bytes in a compact binary frame (length header + JSON metadata + concatenated buffers) instead of being bloated with base64 — no separate upload endpoint needed.
  • Resource Resolvers: ResourceResolver (set via @GET({ resolver: ResourceResolver })) writes a file/resource as the response; ResourceFilter serves static paths.

📦 Installation

pnpm add @dooboostore/simple-boot-http-server reflect-metadata

💻 Usage Example

1. Define a Service (Dependency Injection)

Create a service that contains business logic using @Sim.

import { Sim } from '@dooboostore/simple-boot';

@Sim
export class UserService {
  private users = [{ id: 1, name: 'Dooboo' }];

  getAllUsers() {
    return this.users;
  }
}

2. Configure Router and Controller

Define endpoints using decorators. Inject services through the constructor.

import { Sim, Router, Route } from '@dooboostore/simple-boot';
import { GET, POST } from '@dooboostore/simple-boot-http-server';

@Sim
@Router({ path: '/api' })
export class ApiRouter {
  constructor(private userService: UserService) {}

  @Route({ path: '/users' })
  @GET({ res: { contentType: 'application/json' } })
  getUsers() {
    return this.userService.getAllUsers();
  }
}

3. Run the Server

Configure HttpServerOption and launch the server.

import 'reflect-metadata';
import { SimpleBootHttpServer, HttpServerOption } from '@dooboostore/simple-boot-http-server';

const option = new HttpServerOption(
  { listen: { port: 8080 } },
  { rootRouter: ApiRouter }
);

const server = new SimpleBootHttpServer(option).run();
console.log('🚀 Server running at http://localhost:8080');

🛠️ Advanced Features

Filters

Implement the Filter interface to perform common request processing. A filter has two hooks: proceedBefore runs before the route handler (return false to stop the chain right there), and proceedAfter runs after it (receives before, the boolean proceedBefore returned).

import { Sim } from '@dooboostore/simple-boot';
import { Filter, RequestResponse, SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server';

@Sim
export class MyAuthFilter implements Filter {
    async onInit(app: SimpleBootHttpServer) {}

    async proceedBefore({ rr }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map<string, any> }): Promise<boolean> {
        // Returning false stops the request from proceeding to the next filter/route handler.
        return true;
    }

    async proceedAfter({ rr, before }: { rr: RequestResponse; app: SimpleBootHttpServer; before: boolean; carrier: Map<string, any> }): Promise<boolean> {
        return true;
    }
}

Built-in filters and end points (all exported from the package root):

| Class | Use | |---|---| | ResourceFilter(distPath, [regex \| { request, dist }]) / ResourceFilter([{ request, response }]) | Serve static files or generated data for matching requests | | CacheFilter({ config: { cacheDir?, lifeTime, startUpClean? }, key, filter? }) | Response cache keyed per request | | OpenApi3Filter({ path, excludePath? }, openApiConfig?) | Serve an OpenAPI 3 document generated from routes | | TopicProtocolFilter | Handle the topic protocol over HTTP multipart (X-Simple-Boot-Http-Topic-Protocol: topic) | | IntentSchemeFilter | Route requests to IntentManager | | HttpStatusFilter(status) / ThrowFilter(error) | Respond with a fixed status / throw a fixed error | | HeaderEndPoint(headers) / CrossDomainHeaderEndPoint(config?) | requestEndPoints that add response headers / CORS headers |

Other HttpServerOption fields: serverOption (HTTP/HTTPS), listen, filters, requestEndPoints, closeEndPoints, errorEndPoints, webSocketEndPoints, sessionOption (key, expiredTime, provider), globalAdvice, fileUploadTempPath, noSuchRouteEndPointMappingThrow, transactionManagerFactory.

Generator Streaming (SSE)

Return a generator (function* or async function*) and the server streams it: headers (status, header, contentType from res) are sent immediately, then every yielded value is written to the body right away (strings/Buffers as-is, anything else as JSON), and the response ends when the generator finishes. If the client disconnects, the generator is stopped with return(), so its finally block runs. If the request's Accept-Encoding includes gzip, the stream is gzip-compressed and flushed (Z_SYNC_FLUSH) after every chunk, so events still arrive immediately. Headers are sent before the first chunk, so set them in res or in a before-filter — setHeader after that throws ERR_HTTP_HEADERS_SENT.

@Route({ path: '/events' })
@GET({ res: { contentType: 'text/event-stream', header: { 'Cache-Control': 'no-cache' } } })
async *events(signal: AbortSignal) {
  try {
    while (!signal.aborted) {
      const msg = await nextMessage(signal); // rejects as soon as the client disconnects
      yield `data: ${JSON.stringify(msg)}\n\n`; // SSE framing is up to you — or let ContentTypeGenerator do it (below)
    }
  } finally {
    // runs when the client disconnects — release subscriptions here
  }
}

Every handler can take an AbortSignal parameter (injected by type, like ReqJsonBody). It aborts when the client disconnects before the response finishes. Pass it to anything that waits (fetch, timers/promises, events.once, ...) — a generator suspended on an await isn't stopped by return() until its next yield, so the signal is what releases an event wait. Errors thrown after the client disconnected (e.g. that AbortError) are swallowed.

Content-Type Streams (ContentTypeGenerator)

Yielding raw chunks means writing the wire format yourself — SSE data: lines, multipart boundaries + Content-Length, NDJSON newlines, CSV escaping. ContentTypeGenerator does that part: you hand it the content and get back a stream that carries its own contentType (boundary included) and compress flag. Return it from a handler — no contentType needed in the decorator (if one is set, the stream's wins; other res.headers still apply).

Two ways to feed it:

  • pull — wrap a generator you loop in yourself: CTG.ndjson(rows()). Client disconnect calls return() on your generator, so its finally runs.
  • push — CTG.ndjson.channel({ signal }) gives a stream you push() into from callbacks/events. It queues values and yields them; close() ends the response after the queue drains, and onClose(fn) runs once on close, abort, or client disconnect (even while idle — the place to unsubscribe).
import { ContentTypeGenerator as CTG } from '@dooboostore/simple-boot-http-server';

// pull: MJPEG from a generator of frames (Content-Type / Content-Length per part are filled in)
@Route({ path: '/camera.mjpeg' })
@GET
camera(signal: AbortSignal) {
  return CTG.multipartReplace(this.frames(signal), { boundary: 'frame' });
}

// push: job progress as SSE, released when the client leaves
@Route({ path: '/jobs/progress' })
@GET
progress(signal: AbortSignal) {
  const out = CTG.sse.channel({ signal, heartbeatMs: 15000 });
  const onProgress = (p: number) => out.push({ event: 'progress', data: { p } });
  job.on('progress', onProgress);
  job.once('done', () => out.close());
  out.onClose(() => job.off('progress', onProgress));
  return out;
}

// CSV download that opens in Excel without breaking Korean text
@Route({ path: '/rows.csv' })
@GET({ res: { header: { 'Content-Disposition': 'attachment; filename="rows.csv"' } } })
rows() {
  return CTG.csv(db.cursor(), { columns: ['name', 'memo'], bom: true });
}

| pull / push | Content-Type | You give | Framing added | |---|---|---|---| | sse(src, { heartbeatMs? }) / sse.channel() | text/event-stream | string or { data, event, id, retry, comment } | data: per line (multi-line safe), field lines, blank line; : ping heartbeat while idle | | multipartReplace(src, { boundary?, type? }) / .channel() | multipart/x-mixed-replace; boundary=… | Buffer / Uint8Array / string / { body, type?, name?, headers? } | --boundary, Content-Type, Content-Length (bytes), CRLFs. Not gzipped (frames are already compressed) | | multipartMixed(src, { boundary?, type? }) / .channel() | multipart/mixed; boundary=… | same as above | same, plus --boundary-- at the end | | ndjson(src) / .channel() | application/x-ndjson | any JSON value | JSON + \n | | jsonSeq(src) / .channel() | application/json-seq (RFC 7464) | any JSON value | \x1e JSON \n | | jsonArray(src) / .channel() | application/json | any JSON value | [, , between items, ] — the client just calls res.json() | | csv(src, { columns?, bom? }) / .channel() | text/csv; charset=utf-8 | row arrays, or objects mapped by columns | RFC 4180 quoting, CRLF, header row from columns, optional UTF-8 BOM | | text(src) / .channel() | text/plain; charset=utf-8 | strings (others as JSON) | \n per item |

  • src is any iterable or async iterable (array, function*, async function*).
  • A part's MIME type is resolved as the part's type > mime-types lookup of its name > then for string bodies the stream's type option or text/plain; charset=utf-8, for bytes sniffing (JPEG/PNG/GIF/WEBP) > the type option > application/octet-stream.
  • A missing boundary is randomized; an invalid one (RFC 2046) throws when the stream is created.
  • Channel options: signal (closes on abort), keep: 'queue' (default, every value in order, up to max = 1000, oldest dropped) or keep: 'latest' (only the newest pending value — video frames).
  • jsonArray is valid JSON only once it finishes; for streams that may be cut off, prefer ndjson / jsonSeq.
  • The per-item formatters are exported too: sseChunk, ssePing, multipartPart, multipartEnd, partType, ndjsonLine, jsonSeqRecord, csvCell, csvRow, textLine, boundary, and isStream.

Manual Response Streaming (SSE / NDJSON)

By default, a route's return value is automatically serialized and written as a single response. Set res: 'manual' to skip that entirely and take direct control of the injected ServerResponse — the request handler's promise simply stays pending until you're done, so nothing else writes to the response underneath you.

import { ServerResponse } from 'http';
import { Route } from '@dooboostore/simple-boot';
import { GET } from '@dooboostore/simple-boot-http-server';

@Route({ path: '/stream/time' })
@GET({ res: 'manual' }) // status/header/body auto-write is skipped
streamTime(res: ServerResponse) {
  res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });

  return new Promise<void>(resolve => {
    const timer = setInterval(() => {
      res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
    }, 1000);
    res.on('close', () => { clearInterval(timer); resolve(); }); // clean up when the client disconnects
  });
}

MappingConfig is a union: either res: { status, header, contentType } (+ resolver) or res: 'manual', so TypeScript won't let you combine them — a route is either auto-written or fully manual.

WebSocket: Topic Protocol + Binary File Transfer

Register WebSocketManager as a webSocketEndPoints entry to get a topic-based pub/sub protocol over a single connection — subscribe/unsubscribe, request/response, and server-initiated events the server can await a client reply for.

import { WebSocketManager } from '@dooboostore/simple-boot-http-server/websocket/WebSocketManager';

const option = new HttpServerOption({
  webSocketEndPoints: [WebSocketManager],
  listen: { port: 8080 }
}, { rootRouter: AppRouter });

Any @Sim({ symbol })-tagged service's public methods become callable by name over the socket via an intent target (Symbol.for(ClassName)://methodName) — no extra decorator needed:

@Sim({ symbol: Symbol.for('UserService') })
export class UserService {
  say(message: any) {
    return { echo: message };
  }
}

WebSocketClient (the browser-side counterpart) is not exported from the package root. Import it through the ./* subpath with a bundler (@dooboostore/simple-boot-http-server/websocket/WebSocketClient), or load the standalone UMD bundle dist/umd-bundle/websocket-client.umd.js (built by pnpm run build:client) with a plain <script> tag:

<script src="./node_modules/@dooboostore/simple-boot-http-server/dist/umd-bundle/websocket-client.umd.js"></script>
<script>
  const { WebSocketClient } = dooboostoreSimpleBootHttpServerWebSocketClient;
  const ws = new WebSocketClient('http://localhost:8080', { retryConnectionCount: Infinity, retryConnectionDelay: 1000 });

  const subject = ws.subject('Symbol.for(UserService)://say', { type: 'intent' });
  subject.observable.subscribe({ next: data => console.log('received:', data) });
  subject.send({ hello: 'world' });

  // subject.observable.subscribe(...) returns a *local* RxJS-style subscription —
  // unsubscribing it only stops local delivery. To actually tell the server to
  // drop the topic, call the outer object's own unsubscribe:
  subject.unsubscribe();
</script>

Drop a Buffer (Node) or File/Blob (browser) anywhere inside a message body and it's detected automatically and shipped as raw bytes in a compact binary frame (4-byte length header + JSON metadata with $file references + concatenated buffers) instead of being base64-inflated inside JSON — reconstructed transparently as a real File/{name, mime, size, buffer} on the other side:

say(message: any) {
  const buffer = Buffer.from('hello binary world', 'utf8');
  return { echo: message, file: { name: 'greeting.txt', mime: 'text/plain', buffer } };
}

See the WebSocket Detailed Guide for the full wire protocol.


📖 Learn More

Check out the detailed guides and tutorials in the document folder.


License

MIT License