Skip to main content

Application Structure

In this guide, you'll learn about the structure of a DBOS application.

Directory Structure

When you initialize a DBOS project with npx @dbos-inc/create, it has the following structure:

dbos-hello-app/
├── README.md
├── dbos-config.yaml
├── eslint.config.js
├── jest.config.js
├── knexfile.js
├── migrations/
├── node_modules/
├── package.json
├── package-lock.json
├── src/
│ |── operations.ts
│ └── operations.test.ts
├── start_postgres_docker.js
└── tsconfig.json

The two most important files in a DBOS project are dbos-config.yaml and src/operations.ts.

dbos-config.yaml defines the configuration of a DBOS project, including database connection information, migration configuration, and global logging configuration. All options are documented in our configuration reference.

src/operations.ts is the entrypoint, where DBOS looks for your code. At startup, the DBOS runtime automatically loads all classes that are exported or (directly and indirectly) referenced from this file, serving their endpoints and registering their decorated functions. More precisely, DBOS assumes your compiled code is exported from dist/operations.js, the default location to which src/operations.ts is compiled. If you're writing a small application, you can write all your code directly in this file. In a larger application, you can write your code wherever you want, but should use src/operations.ts as an index file, exporting code written elsewhere:

// Placed in operations.ts:
export { OperationClass1, OperationClass2 } from './FileA';
export { OperationClass3 } from './operations/FileB';

It is not necessary to export classes that are already referenced by the entrypoint file(s), as these will be loaded and decorated methods will be registered. You can also define multiple entrypoint files using the runtimeConfig section of the configuration.

As for the rest of the directory:

  • src/operations.test.ts contains example unit tests written with Jest and our testing runtime. jest.config.js contains Jest configuration.
  • knexfile.js is a configuration file for Knex, which we use as a query builder and migration tool.
  • migrations is initialized with a Knex database migration used in the quickstart guide. You can replace this with your own migration files.
  • node_modules, package.json, package-lock.json, and tsconfig.json are needed by all Node/Typescript projects. eslint.config.js is used by the JavaScript/TypeScript linter, ESLint.
  • start_postgres_docker.js is a convenience script that initializes a Docker Postgres container for use in the quickstart. You can modify this script if you want to use Docker-hosted Postgres for local development.

Code Structure

Here's the initial source code generated by npx @dbos-inc/create (in src/operations.ts):

import { TransactionContext, Transaction, GetApi, ArgSource, ArgSources } from '@dbos-inc/dbos-sdk';
import { Knex } from 'knex';

// The schema of the database table used in this example.
export interface dbos_hello {
name: string;
greet_count: number;
}

export class Hello {

@GetApi('/greeting/:user') // Serve this function from HTTP GET requests to the /greeting endpoint with 'user' as a path parameter
@Transaction() // Run this function as a database transaction
static async helloTransaction(ctxt: TransactionContext<Knex>, @ArgSource(ArgSources.URL) user: string) {
// Retrieve and increment the number of times this user has been greeted.
const query = "INSERT INTO dbos_hello (name, greet_count) VALUES (?, 1) ON CONFLICT (name) DO UPDATE SET greet_count = dbos_hello.greet_count + 1 RETURNING greet_count;";
const { rows } = await ctxt.client.raw(query, [user]) as { rows: dbos_hello[] };
const greet_count = rows[0].greet_count;
return `Hello, ${user}! You have been greeted ${greet_count} times.\n`;
}
}

An application like this one is made up of classes encapsulating functions, written as decorated static class methods. There are four basic types of functions. This example contains two of them:

  • Transactions, like helloTransaction perform database operations.
  • Handlers serve HTTP requests. Here, helloTransaction also acts as a handler, serving HTTP requests from the greeting/:user endpoint.

There are two more:

  • Communicators manage communication with external services and APIs.
  • Workflows reliably orchestrate other functions.

A function needs to follow a few rules:

  • It must be a static class method. For DBOS to find it, that class must be exported or referenced from src/operations.ts (or another file in the runtimeConfig.entrypoints list).
  • It must have a decorator telling the framework what kind of function it is: @Transaction for transactions, @Communicator for communicators, @Workflow for workflows, or GetApi or PostApi for HTTP handlers.
  • Its first argument must be the appropriate kind of context. Contexts provide useful methods, such as access to a database client for transactions.
  • Its input and return types must be serializable to JSON.

Once you've written your functions, there are two basic ways to call them:

  1. Any function (not just handlers) can be called from HTTP if it's annotated with the GetApi or PostApi decorators. See our HTTP serving tutorial for details.
  2. Handlers and workflows can invoke other functions via their contexts' invoke (workflow, handler) method.

To learn more about each individual type of function and what it can do, see our tutorials.