@dooboostore/simple-boot-http-server

NPM versionBuild and TestLicense: MIT

@dooboostore/simple-boot-http-serveris 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@Simdecorator — 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 theFilterinterface.
  • Powerful Exception Handling: Leverage AOP-basedGlobalAdviceto 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:ContentTypeGeneratoradds the wire format for SSE, MJPEG/multipart, NDJSON, JSON Text Sequences, streamed JSON arrays, CSV, and text lines — pull from your generator orpush()into a channel.
  • Manual Response Streaming: Opt out of the automatic response pipeline withres: 'manual'to stream Server-Sent Events, NDJSON, or any long-lived response directly from the injectedServerResponse.
  • Real-time WebSocket Protocol: A topic-based pub/sub protocol over a single socket — subscribe/unsubscribe, request/response,andserver-initiated events the server canawaita client reply for — with automatic reconnection and re-subscription.
  • Binary File Transfer over WebSocket:Buffer/Filevalues 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;ResourceFilterserves 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';@SimexportclassUserService{privateusers = [{id:1,name:'Dooboo'}];getAllUsers() {returnthis.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'})exportclassApiRouter{constructor(privateuserService:UserService) {}@Route({path:'/users'})@GET({res: {contentType:'application/json'} })getUsers() {returnthis.userService.getAllUsers();
  }
}

3. Run the Server

ConfigureHttpServerOptionand launch the server.

import'reflect-metadata';import{SimpleBootHttpServer,HttpServerOption}from'@dooboostore/simple-boot-http-server';constoption =newHttpServerOption(
  {listen: {port:8080} },
  {rootRouter:ApiRouter}
);constserver =newSimpleBootHttpServer(option).run();console.log('🚀 Server running at http://localhost:8080');

🛠️ Advanced Features

Filters

Implement theFilterinterface to perform common request processing. A filter has two hooks:proceedBeforeruns before the route handler (returnfalseto stop the chain right there), andproceedAfterruns after it (receivesbefore, the booleanproceedBeforereturned).

import{Sim}from'@dooboostore/simple-boot';import{Filter,RequestResponse,SimpleBootHttpServer}from'@dooboostore/simple-boot-http-server';@SimexportclassMyAuthFilterimplementsFilter{asynconInit(app:SimpleBootHttpServer) {}asyncproceedBefore({ rr }: {rr:RequestResponse;app:SimpleBootHttpServer;carrier:Map<string,any> }):Promise<boolean> {// Returning false stops the request from proceeding to the next filter/route handler.returntrue;
    }asyncproceedAfter({ rr, before }: {rr:RequestResponse;app:SimpleBootHttpServer;before:boolean;carrier:Map<string,any> }):Promise<boolean> {returntrue;
    }
}

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

ClassUse
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
TopicProtocolFilterHandle the topic protocol over HTTP multipart (X-Simple-Boot-Http-Topic-Protocol: topic)
IntentSchemeFilterRoute requests toIntentManager
HttpStatusFilter(status)/ThrowFilter(error)Respond with a fixed status / throw a fixed error
HeaderEndPoint(headers)/CrossDomainHeaderEndPoint(config?)requestEndPointsthat add response headers / CORS headers

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

Generator Streaming (SSE)

Return a generator (function*orasync function*) and the server streams it: headers (status,header,contentTypefromres) 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 withreturn(), so itsfinallyblock runs. If the request'sAccept-Encodingincludesgzip, 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 inresor in a before-filter —setHeaderafter that throwsERR_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) {constmsg =awaitnextMessage(signal);// rejects as soon as the client disconnectsyield`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 anAbortSignalparameter (injected by type, likeReqJsonBody). 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 anawaitisn't stopped byreturn()until its nextyield, so the signal is what releases an event wait. Errors thrown after the client disconnected (e.g. thatAbortError) are swallowed.

Content-Type Streams (ContentTypeGenerator)

Yielding raw chunks means writing the wire format yourself — SSEdata:lines, multipart boundaries +Content-Length, NDJSON newlines, CSV escaping.ContentTypeGeneratordoes that part: you hand it thecontentand get back a stream that carries its owncontentType(boundary included) andcompressflag. Return it from a handler — nocontentTypeneeded in the decorator (if one is set, the stream's wins; otherres.headers still apply).

Two ways to feed it:

  • pull— wrap a generator you loop in yourself:CTG.ndjson(rows()). Client disconnect callsreturn()on your generator, so itsfinallyruns.
  • push—CTG.ndjson.channel({ signal })gives a stream youpush()into from callbacks/events. It queues values and yields them;close()ends the response after the queue drains, andonClose(fn)runs once on close, abort, or client disconnect (even while idle — the place to unsubscribe).
import{ContentTypeGeneratorasCTG}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'})@GETcamera(signal:AbortSignal) {returnCTG.multipartReplace(this.frames(signal), {boundary:'frame'});
}// push: job progress as SSE, released when the client leaves@Route({path:'/jobs/progress'})@GETprogress(signal:AbortSignal) {constout =CTG.sse.channel({ signal,heartbeatMs:15000});constonProgress= (p:number) => out.push({event:'progress',data: { p } });
  job.on('progress', onProgress);
  job.once('done',() =>out.close());
  out.onClose(() =>job.off('progress', onProgress));returnout;
}// CSV download that opens in Excel without breaking Korean text@Route({path:'/rows.csv'})@GET({res: {header: {'Content-Disposition':'attachment; filename="rows.csv"'} } })rows() {returnCTG.csv(db.cursor(), {columns: ['name','memo'],bom:true});
}
pull / pushContent-TypeYou giveFraming added
sse(src, { heartbeatMs? })/sse.channel()text/event-streamstringor{ data, event, id, retry, comment }data:per line (multi-line safe), field lines, blank line;: pingheartbeat 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 abovesame, plus--boundary--at the end
ndjson(src)/.channel()application/x-ndjsonany JSON valueJSON + \n
jsonSeq(src)/.channel()application/json-seq(RFC 7464)any JSON value\x1e JSON \n
jsonArray(src)/.channel()application/jsonany JSON value[,,between items,]— the client just callsres.json()
csv(src, { columns?, bom? })/.channel()text/csv; charset=utf-8row arrays, or objects mapped bycolumnsRFC 4180 quoting, CRLF, header row fromcolumns, optional UTF-8 BOM
text(src)/.channel()text/plain; charset=utf-8strings (others as JSON)\nper item
  • srcis any iterable or async iterable (array,function*,async function*).
  • A part's MIME type is resolved as the part'stype>mime-typeslookup of itsname> then for string bodies the stream'stypeoption ortext/plain; charset=utf-8, for bytes sniffing (JPEG/PNG/GIF/WEBP) > thetypeoption >application/octet-stream.
  • A missingboundaryis 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 tomax= 1000, oldest dropped) orkeep: 'latest'(only the newest pending value — video frames).
  • jsonArrayis valid JSON only once it finishes; for streams that may be cut off, preferndjson/jsonSeq.
  • The per-item formatters are exported too:sseChunk,ssePing,multipartPart,multipartEnd,partType,ndjsonLine,jsonSeqRecord,csvCell,csvRow,textLine,boundary, andisStream.

Manual Response Streaming (SSE / NDJSON)

By default, a route's return value is automatically serialized and written as a single response. Setres: 'manual'to skip that entirely and take direct control of the injectedServerResponse— 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 skippedstreamTime(res:ServerResponse) {
  res.writeHead(200, {'Content-Type':'text/event-stream','Cache-Control':'no-cache'});returnnewPromise<void>(resolve=>{consttimer =setInterval(() =>{
      res.write(`data:${JSON.stringify({ time:newDate().toISOString() })}\n\n`);
    },1000);
    res.on('close',() =>{clearInterval(timer);resolve(); });// clean up when the client disconnects});
}

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

WebSocket: Topic Protocol + Binary File Transfer

RegisterWebSocketManageras awebSocketEndPointsentry to get a topic-based pub/sub protocol over a single connection — subscribe/unsubscribe, request/response, andserver-initiated events the server canawaita client reply for.

import{WebSocketManager}from'@dooboostore/simple-boot-http-server/websocket/WebSocketManager';constoption =newHttpServerOption({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') })exportclassUserService{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 bundledist/umd-bundle/websocket-client.umd.js(built bypnpm run build:client) with a plain<script>tag:

<scriptsrc="./node_modules/@dooboostore/simple-boot-http-server/dist/umd-bundle/websocket-client.umd.js"></script><script>const{WebSocketClient} = dooboostoreSimpleBootHttpServerWebSocketClient;constws =newWebSocketClient('http://localhost:8080', {retryConnectionCount:Infinity,retryConnectionDelay:1000});constsubject = 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 aBuffer(Node) orFile/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$filereferences + concatenated buffers) instead of being base64-inflated inside JSON — reconstructed transparently as a realFile/{name, mime, size, buffer}on the other side:

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

See theWebSocket Detailed Guidefor the full wire protocol.


📖 Learn More

Check out the detailed guides and tutorials in thedocumentfolder.


License

MIT License