This is examples/full from the dunx repository, running
unmodified on a Raspberry Pi 5 behind a Cloudflare tunnel. Each step
below explains one capability, shows the few lines that declare it,
and has a button that calls the real route: the same one the
example's tests and OpenAPI document are generated from.
It starts with a single request and a single class, and ends with
work that leaves the request entirely: a queue, a broker and a
process watching itself.
Use the arrow keys, or the contents, to move around. The database is
in-memory and reseeds on every restart, so anything created here is
temporary.
Dependency injection
A class asks for what it needs in its constructor, and that is the
whole declaration. No @Injectable(), no
@Inject(), no reflect-metadata:
@dunx/transform reads the parameter types at load time
and the container resolves them.
The source below is read off disk with Bun.file at
/api/demo/source/:name, so it is the file this process
was built from.
Validation
One zod schema per route validates the request and types the
handler's input. A rejected body answers 400 with the issues zod
raised, before the handler runs.
The same schemas generate the OpenAPI document, so nothing is
written twice. Swagger UI is at
/api/docs and Scalar at
/api/reference, both reading one
openapi.json. Each is its own
subpath and optional peer, and both self-host.
API versioning
Two controllers on one path, told apart by version. With
versioning: { type: 'uri' } they serve
/api/v1/swatches and /api/v2/swatches;
header and media-type are the other two
types. @Deprecated makes every v1 response say it is
going.
etag: true hashes every value a GET route
returns into a weak ETag. A client that sends it back
in If-None-Match gets a 304 with no body, and the
handler did not have to know.
Image pipeline
Bun.Image at /api/images/render, returned
as a Response the browser decodes. The source is
generated at runtime from a 4x4 seed, so there is no image file in
the repository.
Guards and roles
One controller, three answers. @Public opts a route
out, a class-level @Roles covers the rest, and a
method-level one overrides it. The guard reads route metadata, so no
handler calls a session helper.
@Roles('admin')
@UseGuards(AuthGuard)
@Controller('reports')
// anyone
@Public() @Get('/health')
// any caller with credentials
@Get('/')
// admin only
@UseGuards(RolesGuard) @Post('/')
Session
better-auth mounted by @dunx/auth, with the anonymous
plugin so there is no form to fill in. Opening this step signed you
in as a guest. The audit route wants a role a guest does not have.
Signed cookies
req.cookies is Bun's own CookieMap, and
SignedCookies adds an HMAC so a value the visitor edits
reads back as absent. The cookie is HttpOnly, so this
page's script cannot see it; the theme below is applied from what
the server reads back.
this.signed.set(req.cookies, 'prefs', theme);
// undefined if the visitor edited it
this.signed.get(req.cookies, 'prefs');
Rate limit
@Throttle({ limit: 3, windowSeconds: 60 }) on
/api/limits/burst. The counter is in Redis when Redis
is up, and in memory when it is not, and every answer carries the
IETF RateLimit headers.
Security headers and CSRF
securityHeaders puts a strict CSP, HSTS,
nosniff and friends on every response, the 404 and the
mapped errors included. csrf refuses a cross-site
unsafe request by the Sec-Fetch-Site header a browser
always sends, with no token to thread through forms.
A page cannot forge that header, so the second button asks the
server to post to /api/users as three callers.
Transactions
/api/ledger/transfer moves an amount between two rows
inside one drizzle transaction. Failing it between the two writes
answers 409, and the unchanged row count is the rollback.
Keyset pagination
@dunx/infra/pagination over a list that is not a
database table. Each page carries a cursor rather than an offset, so
a row inserted between two requests cannot shift the next page.
Cache tiers
@dunx/infra/cache with memory in front of Redis. The
counter is the loader's rather than the cache's, so a second read
moving it by zero is the cache working. DELETE evicts
both tiers.
Idempotency keys
A client that timed out cannot know whether its POST ran.
@Idempotent() lets it retry with the same
Idempotency-Key: the first response is stored and
replayed, and the handler runs once. The store is Redis when it is
up, so a retry on another replica finds it.
@Idempotent()
@Post('/orders', placeOrder)
Server-sent events
@Sse over Bun's own ReadableStream. The
handler is an async generator and every yield is a
frame. Each frame carries an id, so a reconnect resumes rather than
restarting.
Websocket gateway
@Gateway('/chat') on the same Bun.serve as
every route, broadcasting over Bun's native pub/sub. Open this step
in a second tab to see the other side.
RPC on the same port
@dunx/http/connect serves Connect and gRPC-Web as
middleware rather than on a second port, so an RPC gets this app's
CORS, rate limit and request log. A unary Connect call is a POST
with a JSON body, which is all this button sends.
Application events
EventBus over EventTarget: one publisher,
and @OnEvent subscribers in another module. The
response is the dispatch, so a subscriber that throws is reported
rather than swallowed, and the request still succeeds.
Queue
POST /api/jobs/thumbnails enqueues through bullmq over
Bun.RedisClient, and the handler runs in a forked
process. Watch it land in
bull-board.
Message broker
@dunx/infra/amqp over RabbitMQ.
POST /api/messaging/orders publishes to a topic
exchange and the broker routes the key to its own queue, consumed by
this same container. The traceId that comes back is the
one this request carried: a trace that crossed the broker.
Retry and backoff
@dunx/http/client calling an upstream that answers 503
twice before it works. Every attempt the retry policy made comes
back with the millisecond it happened at, so the jittered backoff is
visible rather than described.
Email templates
@dunx/infra/email renders a React Email template and
hands it to whichever EmailTransport is bound, so no
service names a vendor. This demo sends nothing, but the rendered
welcome email is at
/api/email/preview.
Trace context
A W3C traceparent is adopted, so every log line the
request writes carries its trace id and the response carries
traceresponse. OtelModule binds the
Tracer, so routes, queries, jobs and broker messages
open real spans once an OpenTelemetry SDK is registered.
What this process is doing
RequestMetrics, QueryMetrics and
EventLoopLag read out of the running container,
refreshed every five seconds while this step is open. Every button
you pressed on the way here is in the table.