@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/httpAnd 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,
rateLimitMiddleware: 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.
rateLimitMiddleware?: boolean | { // Whether to enable the built-in rate limiting middleware, true to enable with defaults, default: false.
paths?: string[], // Paths to enable rate limiting on uses same path matching as controllers/routes, default: all paths.
limit?: number, // Maximum number of requests allowed within a second per IP and path, default: 5.
},
},
};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
This section covers the available path matching patterns, as of right now we support the following patterns.
Static paths
Static paths are the most basic type of path, they are simply a fixed string that must match exactly, for example:
@Get('/hello')will match only the/hellopath.@Get('/user/profile')will match only the/user/profilepath.
Dynamic (& optional) paths
Dynamic paths are defined using the : syntax, which allows you to capture variable segments of the path. For example:
@Get('/user/:id')will match any path that starts with/user/followed by a dynamicidsegment.@Get('/post/:id/:commentId?')will match paths like/post/123and/post/123/456, capturing both theidand optionalcommentIdsegments.
NOTE
When working with optional paths, make sure to define the parameter as optional!
Glob-style paths
Glob-style paths allow for more complex matching patterns using the * and ** wildcards. For example:
@Get('/user/*')will match any path that starts with/user/followed by any single segment.@Get('/user/**')will match any path that starts with/user/followed by any number of segments.@Get('/files/*.{png,svg}')will match any path that starts with/files/followed by any single segment that ends with.pngor.svg.@Get('/images/**.{jpg,jpeg,png,gif}')will match any path that starts with/images/followed by any number of segments that end with.jpg,.jpeg,.png, or.gif.
If you want to define a glob-style path as a variable, we also have that covered, simply just define a dynamic variable, and in brackets after the variable name, define the glob pattern, like so:
@Get('/files/:filepath(*.png)')will match any path that starts with/files/followed by any single segment that ends with.png, capturing thefilepathsegment.
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
Responseobject, we return it as-is (so you still get full control). - If the value is an instance of
Errorwe shall return a500response with the error message. - If the value is
null, we return a404 Not Foundresponse. - If the value is a
string,number,object, orarray, we return a200response with the value JSON encoded. - If the value is a
booleanand the value istrue, we return a201 Createdresponse with no body. - If the value is a
booleanand the value isfalse, we return a204 No Contentresponse with no body. - If the value is
undefined, we return a204 No Contentresponse 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 asServerfrom thebunpackage.@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 asURLglobal).@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 providingrawas 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.@Context()- Extracts theHttpContextobject for the current request, which allows you to access the request and response's utility class, this is what we used internally to pass the request and response around.@Files(name?: string)- Extracts uploaded files from a multipart/form-data request. If you provide aname, it will extract just that file or files, otherwise, it will provide an object with all files, this will return null for any non-multipart requests.
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() 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.
Built-in Middlewares
We offer some built-in middlewares that can be enabled via the configuration options, we shall expand these over time, but for now, we have the following.
CORS Middleware
By default we offer a very simple CORS middleware, that simply allows all origins, this is mostly for development purposes, and you should implement your own CORS handling for production use cases.
Rate Limiting Middleware
The rate limiting middleware allows you to limit the number of requests a client can make to your server (we use the remote address (IP) and the path) within a certain time frame. This is useful for preventing abuse and ensuring fair usage of your API. This mostly serves as a basic example of how to implement middlewares, and you should consider implementing a more robust solution for production use cases.
TIP
The rate limiting middleware uses an in-memory store to keep track of request counts, which means that the limits will reset if the server is restarted. For production use cases, consider implementing a distributed store (e.g., Redis) to persist the request counts across server restarts and multiple instances.
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/:idwill match/user/123.getRouteParams(route: string, path: string): Record<string, string>- Extracts the route parameters from a given path, e.g./user/:idand/user/123will return{ id: '123' }.parseCookies(cookies: string): Record<string, string>- Parses a cookie header string into an object, e.g.sessionid=abc123; csrftoken=def456will return{ sessionid: 'abc123', csrftoken: 'def456' }.toResponse(value: any, context: HttpContext): Response- Coerces a value into aResponseobject, using the same rules as described in the Return coercing section.
