Skip to content

@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:

bash
bun install @sodacore/http

And then within your main.ts file you can use the plugin like so:

typescript
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:

ts
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:

typescript
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 /hello path.
  • @Get('/user/profile') will match only the /user/profile path.

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 dynamic id segment.
  • @Get('/post/:id/:commentId?') will match paths like /post/123 and /post/123/456, capturing both the id and optional commentId segments.

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 .png or .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 the filepath segment.

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 a 500 response with the error message.
  • If the value is null, we return a 404 Not Found response.
  • If the value is a string, number, object, or array, we return a 200 response with the value JSON encoded.
  • If the value is a boolean and the value is true, we return a 201 Created response with no body.
  • If the value is a boolean and the value is false, we return a 204 No Content response with no body.
  • If the value is undefined, we return a 204 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 as Request (global object).
  • @Server() - You can access the direct server instance, typed as Server from the bun package.
  • @Params(name?: string) - If you define route parameters (e.g. /user/:id), you can extract them via this decorator. If you provide a name, 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 as URL 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 providing raw 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.
  • @Context() - Extracts the HttpContext object 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 a name, 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:

typescript
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:

ts
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 a Response object, using the same rules as described in the Return coercing section.

Released under the Apache-2.0 License.