HTTP Serving
In this guide, you'll learn how to make DBOS applications accessible through DBOS's built-in, koa-based HTTP server.
There is no need to use the DBOS HTTP server. DBOS functions can be embedded in other HTTP servers, and there is no need to use HTTP in DBOS applications at all. However, the DBOS HTTP handler service is the simplest way to get started in a new HTTP application, offering simple route handling, parameter parsing and validation, security, logging, etc.
Any function can be made into an HTTP endpoint by annotating it with an endpoint decorator, causing DBOS to use that function to serve that endpoint.
You can apply an endpoint decorator either to a new function without any other decorators or to an existing function with a @DBOS.transaction
, @DBOS.workflow
, or DBOS.step
decorator.
Here's an example of a new function with an endpoint decorator:
@DBOS.getApi('/greeting/:name')
static async greetingEndpoint(name: string) {
return `Greeting, ${name}`;
}
Here's an example applying an endpoint decorator to an existing transaction (the order of the decorators doesn't matter):
@DBOS.postApi('/greeting/:friend')
@DBOS.transaction()
static async insertGreeting(friend: string, note: string) {
await DBOS.knexClient.raw('INSERT INTO greetings (name, note) VALUES (?, ?)', [friend, note]);
}
DBOS provides endpoint decorators for all HTTP verbs used in APIs: @DBOS.getApi
, @DBOS.postApi
, @DBOS.putApi
, @DBOS.patchApi
, and @DBOS.deleteApi
.
Each associates a function with an HTTP URL.
Inputs and HTTP Requests
When a function has arguments other than its context, DBOS automatically parses them from the HTTP request, and returns an error to the client if they are not found.
Arguments can be parsed from three places:
1. URL Path Parameters
You can include a path parameter placeholder in a URL by prefixing it with a colon, like name
in this example:
@DBOS.getApi('/greeting/:name')
static async greetingEndpoint(name: string) {
return `Greeting, ${name}`;
}
Then, give your method an argument with a matching name (such as name: string
above) and it is automatically parsed from the path parameter.
For example, if we send our app this request, then our method is called with name
set to dbos
:
GET /greeting/dbos
2. URL Query String Parameters
GET
and DELETE
endpoints automatically parse arguments from query strings.
For example, the following endpoint expects the id
and name
parameters to be passed through a query string:
@DBOS.getApi('/example')
static async exampleGet(id: number, name: string) {
return `${id} and ${name} are parsed from URL query string parameters`;
}
If we send our app this request, then our method is called with id
set to 123
and name
set to dbos
:
GET /example?id=123&name=dbos
3. HTTP Body Fields
POST
, PATCH
, and PUT
endpoints automatically parse arguments from the HTTP request body.
For example, the following endpoint expects the id
and name
parameters to be passed through the HTTP request body:
@DBOS.postApi('/example')
static async examplePost(id: number, name: string) {
return `${id} and ${name} are parsed from the HTTP request body`;
}
If we send our app this request, then our method is called with id
set to 123
and name
set to dbos
:
POST /example
Content-Type: application/json
{
"name": "dbos",
"id": 123
}
When sending an HTTP request with a JSON body, make sure you set the Content-Type
header to application/json
.
Raw Requests
If you need finer-grained request parsing, any DBOS method invoked via HTTP request can access raw request information from DBOS.request
. This returns the following information:
interface HTTPRequest {
readonly headers?: IncomingHttpHeaders; // A node's http.IncomingHttpHeaders object.
readonly rawHeaders?: string[]; // Raw headers.
readonly params?: unknown; // Parsed path parameters from the URL.
readonly body?: unknown; // parsed HTTP body as an object.
readonly rawBody?: string; // Unparsed raw HTTP body string.
readonly query?: ParsedUrlQuery; // Parsed query string.
readonly querystring?: string; // Unparsed raw query string.
readonly url?: string; // Request URL.
readonly ip?: string; // Request remote address.
}
Outputs and HTTP Responses
If a function invoked via HTTP request returns successfully, its return value is sent in the HTTP response body with status code 200
(or 204
if nothing is returned).
If the function throws an exception, the error message is sent in the response body with a 400
or 500
status code.
If the error contains a status
field, the response uses that status code instead.
If you need custom HTTP response behavior, you can use a handler to access the HTTP response directly.
DBOS uses Koa for HTTP serving internally and the raw response can be accessed via DBOS.koaContext.response
, which provides a Koa response.
Handlers
A function annotated with an endpoint decorator but no other decorators is called a handler. Handlers can call other DBOS functions and directly access HTTP requests and responses. However, DBOS makes no guarantees about handler execution: if a handler fails, it is not automatically retried. You should use handlers when you need to access HTTP requests or responses directly or when you are writing a lightweight task that does not need the strong guarantees of transactions and workflows.
Body Parser
By default, DBOS uses @koa/bodyparser
to support JSON in requests. If this default behavior is not desired, you can configure a custom body parser with the @KoaBodyParser
decorator.
import { bodyParser } from "@koa/bodyparser";
@KoaBodyParser(bodyParser({
extendTypes: {
json: ["application/json", "application/custom-content-type"],
},
encoding: "utf-8"
}))
class OperationEndpoints {
}
CORS
Cross-Origin Resource Sharing (CORS) is a security feature that controls access to resources from different domains. DBOS uses @koa/cors
with a permissive default configuration.
If more complex logic is needed, or if the CORS configuration differs between operation classes, the @KoaCors
class-level decorator can be used to specify the CORS middleware in full.
import cors from "@koa/cors";
@KoaCors(cors({
credentials: true,
origin:
(o: Context)=>{
const whitelist = ['https://us.com','https://partner.com'];
const origin = o.request.header.origin ?? '*';
return (whitelist.includes(origin) ? origin : '');
}
}))
class EndpointsWithSpecialCORS {
}
Middleware
DBOS supports running arbitrary Koa middleware for serving HTTP requests.
Middlewares are configured at the class level through the @KoaMiddleware
decorator.
Here is an example of a simple middleware checking an HTTP header:
import { Middleware } from "koa";
const middleware: Middleware = async (ctx, next) => {
const contentType = ctx.request.headers["content-type"];
await next();
};
@KoaMiddleware(middleware)
class Hello {
...
}
Global Middleware
The @KoaMiddleware
decorator above only places middleware on registered routes within the decorated class. It is sometimes desired to add middleware to all routes globally, including on routes that are not registered at all.
For example, to install logging that would pick up on 404 errors or other bad requests during development, koa-logger
could be installed as a global middleware
import logger from 'koa-logger';
@KoaGlobalMiddleware(logger())
class OperationEndpoints{
...
}
If more detail is required, or to ensure that logs go to the DBOS log, a custom logger can be implemented:
// Logging middleware
const logAllRequests = () => {
return async (ctx: Koa.Context, next: Koa.Next) => {
const start = Date.now();
// Log the request method and URL
DBOS.logger.info(`[Request] ${ctx.method} ${ctx.url}`);
let ok = false;
try {
await next();
ok = true;
}
finally {
const ms = Date.now() - start;
if (ok) {
// Log the response status and time taken
DBOS.logger.info(`[Response] ${ctx.method} ${ctx.url} - ${ctx.status} - ${ms}ms`);
}
else {
// Log error response
DBOS.logger.warn(`[Exception] ${ctx.method} ${ctx.url} - ${ctx.status} - ${ms}ms`);
}
}
};
};
@KoaGlobalMiddleware(logAllRequests())
class OperationEndpoints{
...
}
@Authentication
Configures the DBOS HTTP server to perform authentication. All functions in the decorated class will use the provided function to act as an authentication middleware. This middleware will make users' identity available to DBOS functions. Here is an example:
async function exampleAuthMiddleware (ctx: MiddlewareContext) {
if (ctx.requiredRole.length > 0) {
const { userid } = ctx.koaContext.request.query;
const uid = userid?.toString();
if (!uid || uid.length === 0) {
const err = new DBOSNotAuthorizedError("Not logged in.", 401);
throw err;
}
else {
if (uid === 'bad_person') {
throw new DBOSNotAuthorizedError("Go away.", 401);
}
return {
authenticatedUser: uid,
authenticatedRoles: (uid === 'a_real_user' ? ['user'] : ['other'])
};
}
}
}
@Authentication(exampleAuthMiddleware)
class OperationEndpoints {
@DBOS.getApi("/requireduser")
@DBOS.requiredRole(['user'])
static async checkAuth() {
return `Please say hello to ${DBOS.authenticatedUser}`;
}
}
The interface for the authentication middleware is:
/**
* Authentication middleware executing before requests reach functions.
* Can implement arbitrary authentication and authorization logic.
* Should throw an error or return an instance of `DBOSHttpAuthReturn`
*/
export type DBOSHttpAuthMiddleware = (ctx: MiddlewareContext) => Promise<DBOSHttpAuthReturn | void>;
export interface DBOSHttpAuthReturn {
authenticatedUser: string;
authenticatedRoles: string[];
}
The authentication function is provided with a 'MiddlewareContext', which allows access to the request, system configuration, logging, and database access services.
MiddlewareContext
MiddlewareContext
is provided to functions that execute against a request before entry into handler, transaction, and workflow functions. These middleware functions are generally executed before, or in the process of, user authentication, request validation, etc. The context is intended to provide read-only database access, logging services, and configuration information.
Properties and Methods
MiddlewareContext.logger
readonly logger: DBOSLogger;
logger
is available to record any interesting successes, failures, or diagnostic information that occur during middleware processing.
MiddlewareContext.span
readonly span: Span;
span
is the tracing span in which the middleware is being executed.
MiddlewareContext.koaContext
readonly koaContext: Koa.Context;
koaContext
is the Koa context, which contains the inbound HTTP request associated with the middleware invocation.
MiddlewareContext.name
readonly name: string;
name
contains the name of the function (handler, transaction, workflow) to be invoked after successful middleware processing.
MiddlewareContext.requiredRole
readonly requiredRole: string[];
requiredRole
contains the list of roles required for the invoked operation. Access to the function will granted if the user has any role on the list. If the list is empty, it means there are no authorization requirements and may indicate that authentication is not required.
MiddlewareContext.getConfig
getConfig<T>(key: string, deflt: T | undefined) : T | undefined
getConfig
retrieves configuration information (from .yaml config file / environment). If key
is not present in the configuration, defaultValue
is returned.
MiddlewareContext.query
query<C extends UserDatabaseClient, R, T extends unknown[]>(qry: (dbclient: C, ...args: T) => Promise<R>, ...args: T): Promise<R>;
The query
function provides read access to the database.
To provide a scoped database connection and to ensure cleanup, the query
API works via a callback function.
The application is to pass in a qry
function that will be executed in a context with access to the database client dbclient
.
The provided dbClient
will be a Knex
or TypeORM EntityManager
or PrismaClient
depending on the application's choice of SQL access library.
This callback function may take arguments, and return a value.
Example, for Knex:
const u = await ctx.query(
// The qry function that takes in a dbClient and a list of arguments (uname in this case)
(dbClient: Knex, uname: string) => {
return dbClient<UserTable>(userTableName).select("username").where({ username: uname })
},
userName // Input value for the uname argument
);
Serving Static Content
While it is generally advised to serve static content from a CDN, S3 Bucket, or similar, sometimes it is desired to serve a few static files from DBOS. This can be done easily with a package such as koa-send
:
export class Serve {
@DBOS.getApi('/static/:item') // Adjust route, or recursive wildcard, to suit
static async serve(item: string) {
return send(DBOS.koaContext, item, {root: 'static'}); // Adjust root to point to directory w/ files
}
}
To install koa-send
:
npm install koa-send; npm install --save-dev @types/koa-send