@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.
@Router,@Route,@GET,@POST,@PUT,@DELETE,@PATCH.@Simdecorator — the exact same DI/AOP container as@dooboostore/simple-boot.Filterinterface.GlobalAdviceto isolate business logic from error handling logic.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.res: 'manual'to stream Server-Sent Events, NDJSON, or any long-lived response directly from the injectedServerResponse.awaita client reply for — with automatic reconnection and re-subscription.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.ResourceResolver(set via@GET({ resolver: ResourceResolver })) writes a file/resource as the response;ResourceFilterserves static paths.pnpm add @dooboostore/simple-boot-http-server reflect-metadataCreate a service that contains business logic using@Sim.
import{Sim}from'@dooboostore/simple-boot';@SimexportclassUserService{privateusers = [{id:1,name:'Dooboo'}];getAllUsers() {returnthis.users;
}
}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();
}
}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');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):
| 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 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.
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.
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:
CTG.ndjson(rows()). Client disconnect callsreturn()on your generator, so itsfinallyruns.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 / push | Content-Type | You give | Framing added |
|---|---|---|---|
sse(src, { heartbeatMs? })/sse.channel() | text/event-stream | stringor{ 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 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 callsres.json() |
csv(src, { columns?, bom? })/.channel() | text/csv; charset=utf-8 | row arrays, or objects mapped bycolumns | RFC 4180 quoting, CRLF, header row fromcolumns, optional UTF-8 BOM |
text(src)/.channel() | text/plain; charset=utf-8 | strings (others as JSON) | \nper item |
srcis any iterable or async iterable (array,function*,async function*).type>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.boundaryis randomized; an invalid one (RFC 2046) throws when the stream is created.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.sseChunk,ssePing,multipartPart,multipartEnd,partType,ndjsonLine,jsonSeqRecord,csvCell,csvRow,textLine,boundary, andisStream.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.
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.
Check out the detailed guides and tutorials in thedocumentfolder.