HTTP Serving
In this guide, you'll learn how to make DBOS applications accessible through HTTP.
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 an @Transaction
, @Workflow
, or @Step
decorator.
Here's an example of a new function with an endpoint decorator:
@GetApi('/greeting/:name')
static async greetingEndpoint(ctx: HandlerContext, 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):
@PostApi('/greeting/:friend')
@Transaction()
static async insertGreeting(ctxt: TransactionContext<Knex>, friend: string, note: string) {
await ctxt.client.raw('INSERT INTO greetings (name, note) VALUES (?, ?)', [friend, note]);
}
DBOS provides endpoint decorators for all HTTP verbs used in APIs: @GetApi
, @PostApi
, @PutApi
, @PatchApi
, and @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:
@GetApi('/greeting/:name')
static async greetingEndpoint(ctx: HandlerContext, 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:
@GetApi('/example')
static async exampleGet(ctx: HandlerContext, 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:
@PostApi('/example')
static async examplePost(ctx: HandlerContext, 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
.
No matter where arguments are parsed from, if arguments are not supplied or are of the wrong type, the endpoint will throw an input validation error.
You can specify that an argument is optional with the @ArgOptional
parameter decorator.
You can use the @ArgSource()
parameter decorator to parse an argument from a non-default location (for example, from a query string in a POST
handler).
Raw Requests
If you need finer-grained request parsing, any DBOS method invoked via HTTP request can access raw request information from its context.request
field. 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 the .koaContext.response
field of HandlerContext
, which provides a Koa response.
Handlers
A function annotated with an endpoint decorator but no other decorators is called a handler and must take a HandlerContext
as its first argument, like in the first example above.
Handlers can invoke other 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.
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.
To customize CORS:
- Use the HTTP configuration in
dbos-config.yaml
for global settings. - Use the
@KoaCors
class decorator for class-specific settings.
Middleware
DBOS supports running custom 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.globalLogger?.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.globalLogger?.info(`[Response] ${ctx.method} ${ctx.url} - ${ctx.status} - ${ms}ms`);
}
else {
// Log error response
DBOS.globalLogger?.warn(`[Exception] ${ctx.method} ${ctx.url} - ${ctx.status} - ${ms}ms`);
}
}
};
};
@KoaGlobalMiddleware(logAllRequests())
class OperationEndpoints{
...
}
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 {
@GetApi('/static/:item') // Adjust route, or recursive wildcard, to suit
static async serve(ctx: HandlerContext, item: string) {
return send(ctx.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