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,
	},
}));

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.
	},
};

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

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 for id.
    • /images/:name(*.png) matches only PNG files for name.
  • 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

PatternPathParams 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 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.

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:

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.

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:

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.