@sodacore/http
The @sodacore/http
package provides a series of decorators and helpers to quickly build HTTP servers and APIs.
Installation
To install the plugin, simply install it via bun:
bun install @sodacore/http
And then within your main.ts
file you can use the plugin like so:
import { Application } from '@sodacore/core';
import HttpPlugin from '@sodacore/http';
const app = new Application({
autowire: true,
});
app.use(new HttpPlugin({
port: 3000,
ssePath: '/events',
builtin: {
corsMiddleware: true,
},
}));
app.start().catch(console.error);
Configuration
The main plugin, accepts the following configuration options:
export type IConfig = {
port: number, // Default port to bind to, default: 8080.
host?: string, // Optional host to bind to, default: '0.0.0.0'.
ssePath?: string, // Global path to bind the SSE server to, default: undefined (disabled).
httpOptions?: {
maxRequestBodySize?: number, // Max request body size in bytes, default: 1024 * 1024 * 128 // 128MB.
idleTimeout?: number, // Idle timeout, default: 1024 * 10 // 10s.
ipv6Only?: boolean, // Whether to bind to IPv6 only, default: false.
reusePort?: boolean, // Allows the port to be reused, good for load balancing, default: false.
unixSocketPath?: string, // Whether to bind to a unix socket path instead of a TCP port, default: undefined.
tls?: TLSOptions, // TLS options, if defined, will allow HTTPS connections directly, default: undefined.
},
builtin?: {
corsMiddleware?: boolean, // Whether to enable the built-in CORS middleware, default: false.
},
};
Controllers
To create a controller, you can use the below example:
import { Controller, Get } from '@sodacore/core';
@Controller('/hello')
export class HelloController {
@Get('/')
public async hello() {
return 'Hello, world!';
}
}
The @Get
decorator defines the HTTP method and path (which is appended to the @Controller
defined path), to execute for.
Path Patterns
Route Pattern Matching Documentation
This module provides advanced route matching and parameter extraction for path patterns. Supported features:
Supported Path Patterns
- Static segments:
/home
,/about
,/contact
- Nested paths:
/users/:userId/posts/:postId/comments
- Dynamic variables:
:variableName
(e.g.,/products/:productId
)
- Single-segment wildcards:
*
(e.g.,/files/*/details
matches/files/abc/details
)
- Multi-segment wildcards:
**
(e.g.,/assets/**
matches/assets/images/icons/foo.png
)
- Regex patterns for dynamic segments:
:variableName(regex)
(e.g.,/items/:itemId(\d+)
matches only numeric IDs)
- Glob-style patterns for segments:
:variableName(glob)
(e.g.,/images/:imageName(*.png)
matches only PNG files)
- Optional segments (only at end):
?
(e.g.,/profile/:username?
matches with or without username)
Matching Logic
- Static segments must match exactly.
- Dynamic segments (e.g.,
:id
) match any non-slash string and are extracted as parameters. - Wildcards:
*
matches a single path segment.**
matches any number of segments (including slashes).
- Regex and glob patterns can restrict dynamic segments:
/items/:id(\d+)
matches only numbers forid
./images/:name(*.png)
matches only PNG files forname
.
- Optional segments are only allowed at the end of the route.
- Trailing globs (e.g.,
/files/:fileName(*)
) will match everything after that segment, including slashes.
Examples
Pattern | Path | Params Extracted |
---|---|---|
/users/:userId/posts/:postId/comments | /users/123/posts/456/comments | { userId: '123', postId: '456' } |
/files/*/details | /files/image/details | |
/assets/** | /assets/images/icons/logo.png | |
/items/:itemId(\d+) | /items/12345 | { itemId: '12345' } |
/images/:imageName(*.png) | /images/photo.png | { imageName: 'photo.png' } |
/profile/:username/settings | /profile/johndoe/settings | { username: 'johndoe' } |
/files/:fileName(*) | /files/asfsfasf/document.pdf | { fileName: 'asfsfasf/document.pdf' } |
TIP
You can also do @Get('/assets/:path')
and then extract the path as @Params('path') path: string
, which will give you the full path after /assets/
, e.g. image.png
, css/style.css
, etc, as our dynamic routing will match anything after the defined segments if nothing comes after, it gets treated as "everything else".
Return coercing
When you return data via the controller methods, we try to do some kind of coercing to a proper response:
- If the value is a
Response
object, we return it as-is (so you still get full control). - If the value is an instance of
Error
we shall return a500
response with the error message. - If the value is
null
, we return a404 Not Found
response. - If the value is a
string
,number
,object
, orarray
, we return a200
response with the value JSON encoded. - If the value is a
boolean
and the value istrue
, we return a201 Created
response with no body. - If the value is a
boolean
and the value isfalse
, we return a204 No Content
response with no body. - If the value is
undefined
, we return a204 No Content
response with no body.
As you can see, we try to define some sensible defaults, but if you need more control, you can easily return a Response
object directly.
Available Decorators
The current supported method decorators are:
@Get(path)
@Post(path)
@Put(path)
@Patch(path)
@Delete(path)
@Options(path)
@Head(path)
Parameter Decorators
On top of this, we also offer parameter decorators to extract data from the request:
@Request()
- Every request has a request object, this decorator will allow you to inject it, typed asRequest
(global object).@Server()
- You can access the direct server instance, typed asServer
from thebun
package.@Params(name?: string)
- If you define route parameters (e.g./user/:id
), you can extract them via this decorator. If you provide aname
, it will extract just that parameter, otherwise, it will provide an object with all parameters.@Query(name?: string)
- Similar to@Params
, but for query parameters (e.g./user?id=123
).@Headers(name?: string)
- Similar to@Params
, but for headers (e.g.Authorization: Bearer ...
).@Cookies(name?: string)
- Similar to@Params
, but for cookies (e.g.sessionid=abc123
).@Url()
- Extracts the full URL object (typed asURL
global).@Body(format: 'json' | 'raw' = 'json')
- Extracts the body of the request. By default, we assume it's JSON and will parse it as such, but you can also extract it as raw text by providingraw
as the format.@Method()
- Extracts the HTTP method (e.g.GET
,POST
, etc), you can apply many method decorators to the same controller method, so this can be useful for distinguishing them.
Server-Sent Events (Eventsource / SSE)
The package does implement a basic SSE server, that sets itself against an endpoint, if you define an ssePath
in the configuration.
The way it works is that when a user connects to the SSE endpoint, they will be added and registered, the system will deal with the connection/disconnect events as necessary.
Sending data
To send data, you can simply inject the SseConnectionsProvider
provider which offers the core SSE connection management, but also allows you to access broadcasting methods, like;
broadcast(packet: ISsePacket)
- Broadcasts a packet to all connected clients.broadcastRaw(data: string)
- Broadcasts raw data to all connected clients (you must format it yourself).broadcastFor(id: string | string[], packet: ISsePacket)
- Broadcasts a packet to a specific client or clients by ID.broadcastRawFor(id: string | string[], data: string)
- Broadcasts raw data to a specific client or clients by ID (you must format it yourself).broadcastExcept(id: string | string[], packet: ISsePacket)
- Broadcasts a packet to all connected clients except the given ID or IDs.broadcastRawExcept(id: string | string[], data: string)
- Broadcasts raw data to all connected clients except the given ID or IDs (you must format it yourself).
And then additional functionality for managing connections if needed, this available through type-hinting within your editor, but we don't expect you to use those as the plugin manages them automatically.
Dynamic paths
If you want to allow connections to specific paths, then you can actually create an SseContext
object and simply return the getSseResponse(userHeaders)
method automatically, see below:
import type { Server } from 'bun';
import { Controller, Get, SseContext } from '@sodacore/http';
@Controller('/events')
export class EventsController {
@Get('/stream/:roomId')
public async stream(
@Request() request: Request,
@Server() server: Server,
) {
return new SseContext(request, server).getSseResponse();
}
}
The SseContext
automatically handles the adding/removing of connections for you, and it automatically deals with the heartbeat required to keep the connection alive.
Middlewares
Middlewares are a way to run code before a request is handled by a controller method, they can be used for things like authentication, logging, etc.
Middlewares are documented in more details in the Middlewares section of the docs.
By default, we provide a very stupid-simple CORS middleware that allows all origins, this is mostly for development purposes, and you should implement your own CORS handling for production use cases, as noted in the Configuration section can be easily enabled on-demand.
Transformers
Transformers are a way to modify the request and response objects before they are handled by a controller method. They can be used for things like input validation, output formatting, etc.
Transformers are documented in more details in the Transformers section of the docs.
Hooks
This package also provides a few available hooks that can be used to run code when specific things happen, as of right now, the available hooks are:
httpRequest
- Called when an HTTP request is received, before any routing or middlewares have fired.sseRequest
- Called when an SSE request is received, before any routing or middlewares have fired.
IMPORTANT
If you use your own dynamic SSE paths, the sseRequest
hook will not be called, as the request is handled directly by the controller method and not by the framework, but you can always implement your own hooks within your controller method if needed.
See the Hooks section of the docs for more information on how to create your own hooks.
Utilities
The HTTP plugin also provides a set of utility functions under the Utils
namespace, that can be imported like so:
import { Utils } from '@sodacore/http';
The available utilities are:
doesRouteMatch(path: string, value: string): boolean
- Checks if a given path matches a route pattern, e.g./user/:id
will match/user/123
.getRouteParams(route: string, path: string): Record<string, string>
- Extracts the route parameters from a given path, e.g./user/:id
and/user/123
will return{ id: '123' }
.parseCookies(cookies: string): Record<string, string>
- Parses a cookie header string into an object, e.g.sessionid=abc123; csrftoken=def456
will return{ sessionid: 'abc123', csrftoken: 'def456' }
.toResponse(value: any, context: HttpContext): Response
- Coerces a value into aResponse
object, using the same rules as described in the Return coercing section.