Skip to main content

Programming Guide

Let's learn how to build applications with DBOS Transact. In this tutorial, we'll modify the example application from our quickstart to reliably send greetings to your friends. We'll show you how construct a reliable workflow that updates your database and calls third-party APIs.

Before starting this tutorial, we recommend finishing the quickstart. You can use the application from the quickstart to complete this tutorial.

Serving Your Applications

what you will learn

How to serve your application via HTTP.

Let's add an HTTP GET handler to your application so it can send greetings to your friends. Add this code to src/operations.ts:

import { HandlerContext, GetApi } from '@dbos-inc/dbos-sdk'

export class Greetings {
@GetApi('/greeting/:friend')
static async Greeting(ctxt: HandlerContext, friend: string) {
return `Greetings, ${friend}!`;
}
}

Rebuild with npm run build and start your application with npx dbos start. You should see an output similar to:

[info]: Workflow executor initialized
[info]: HTTP endpoints supported:
[info]: GET : /greeting/:friend
[info]: Kafka endpoints supported:
[info]: Scheduled endpoints:
[info]: DBOS Server is running at http://localhost:3000
[info]: DBOS Admin Server is running at http://localhost:3001

To see that your application is working, visit this URL in your browser: http://localhost:3000/greeting/Mike. You should see the message Greetings, Mike!. If you replace Mike with a different name, your application will greet that name instead.

The key to this code is the @GetApi decorator, which tells DBOS to serve the Greeting function from HTTP GET requests to the /greeting endpoint. As you will see, DBOS relies on decorators to simplify your programming experience. To load these decorators, DBOS methods must be static class members. In this case, Greeting is a static member of the Greetings class.

To learn more about HTTP serving in DBOS, see our guide.

Creating Database Tables

what you will learn

How to create and manage database tables.

Let's create a database table our app can use to store greetings. In DBOS, we recommend managing database tables using schema migrations. By default, we use Knex to manage migrations, but also support other tools including TypeORM and Prisma. To create a new migration file, run the following command:

npx knex migrate:make greetings

This will create a new file named migrations/<timestamp>_greetings.js. Open that file and copy the following code into it:

exports.up = function(knex) {
return knex.schema.createTable('greetings', table => {
table.text('name');
table.text('note');
});
};


exports.down = function(knex) {
return knex.schema.dropTable('greetings');
};

This code instructs the database to create a new table called greetings with two text columns: a name column to store the person being greeted and a note column to store the greeting sent to them. To run this code and create the new table, run:

npx dbos migrate

If successful, the migration should print Migration successful!. To learn more about schema migrations in DBOS, check out our guides for Knex, TypeORM, and Prisma.

Connecting to the Database

what you will learn

How to interact with the database.

Now, let's insert a record into this new table when we greet a friend. We'll do this with a transactional function. Copy this code into src/operations.ts:

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

export class Greetings {
@Transaction()
static async InsertGreeting(ctxt: TransactionContext<Knex>, friend: string, note: string) {
await ctxt.client.raw('INSERT INTO greetings (name, note) VALUES (?, ?)', [friend, note]);
ctxt.logger.info(`Greeting to ${friend} recorded in the database!`);
}

@GetApi('/greeting/:friend')
static async Greeting(ctxt: HandlerContext, friend: string) {
const noteContent = `Thank you for being awesome, ${friend}!`;
await ctxt.invoke(Greetings).InsertGreeting(friend, noteContent);
return noteContent;
}
}

The key elements of this code are:

  • We use the @Transaction decorator to define a transactional function (InsertGreeting) that accesses the database from a managed client.
  • Inside InsertGreeting, we insert a row in the database with ctxt.client.raw().
  • We add a line to Greeting invoking InsertGreeting.

To learn more about accessing the database in DBOS, see our guide.

info

In this quickstart, we write our database operations in raw SQL (using knex.raw), but DBOS Transact also supports knex's query builder, TypeORM, and Prisma.

Interacting with External Services

what you will learn

How to safely send requests to third-party APIs.

Let's say we want to use a third-party client to send our greeting via e-mail. In DBOS, we strongly recommend wrapping calls to third-party APIs in Communicators. We'll see in the next section how communicators make your code more reliable. For now, add this code to src/operations.ts:

import {
TransactionContext, Transaction,
HandlerContext, GetApi,
CommunicatorContext, Communicator,
} from "@dbos-inc/dbos-sdk";
import { Knex } from "knex";

export class Greetings {
@Communicator()
static async SendGreetingEmail(ctxt: CommunicatorContext, friend: string, content: string) {
ctxt.logger.info(`Sending email "${content}" to ${friend}...`);
// Code omitted for simplicity
ctxt.logger.info("Email sent!");
}

@Transaction()
static async InsertGreeting(ctxt: TransactionContext<Knex>, friend: string, note: string) {
await ctxt.client.raw('INSERT INTO greetings (name, note) VALUES (?, ?)', [friend, note]);
ctxt.logger.info(`Greeting to ${friend} recorded in the database!`);
}

@GetApi("/greeting/:friend")
static async Greeting(ctxt: HandlerContext, friend: string) {
const noteContent = `Thank you for being awesome, ${friend}!`;
await ctxt.invoke(Greetings).SendGreetingEmail(friend, noteContent);
await ctxt.invoke(Greetings).InsertGreeting(friend, noteContent);
return noteContent;
}
}

The key elements of this code are:

  • We use the @Communicator decorator to define a communicator function (SendGreetingEmail) to access a third-party email service.
  • We add a line to Greeting invoking SendGreetingEmail.

To learn more about communication with external services and APIs in DBOS, see our guide.

Composing Reliable Workflows

what you will learn

How to make your applications reliable using DBOS workflows.

Next, we want to make our app reliable: guarantee that it inserts exactly one database record per greeting email sent, even if there are transient failures or service interruptions. DBOS makes this easy with workflows. To see them in action, add this code to src/operations.ts:

import {
TransactionContext, Transaction, GetApi,
CommunicatorContext, Communicator,
WorkflowContext, Workflow,
} from "@dbos-inc/dbos-sdk";
import { Knex } from "knex";

export class Greetings {
@Communicator()
static async SendGreetingEmail(ctxt: CommunicatorContext, friend: string, content: string) {
ctxt.logger.info(`Sending email "${content}" to ${friend}...`);
// Code omitted for simplicity
ctxt.logger.info("Email sent!");
}

@Transaction()
static async InsertGreeting(ctxt: TransactionContext<Knex>, friend: string, note: string) {
await ctxt.client.raw('INSERT INTO greetings (name, note) VALUES (?, ?)', [friend, note]);
ctxt.logger.info(`Greeting to ${friend} recorded in the database!`);
}

@Workflow()
@GetApi("/greeting/:friend")
static async GreetingWorkflow(ctxt: WorkflowContext, friend: string) {
const noteContent = `Thank you for being awesome, ${friend}!`;
await ctxt.invoke(Greetings).SendGreetingEmail(friend, noteContent);

for (let i = 0; i < 5; i++) {
ctxt.logger.info(
"Press Control + C to interrupt the workflow..."
);
await ctxt.sleepms(1000);
}

await ctxt.invoke(Greetings).InsertGreeting(friend, noteContent);
return noteContent;
}
}

The key elements of this snippet are:

  • We create a workflow function (GreetingWorkflow) using the @Workflow decorator. We move the @GetApi decorator to this function to serve HTTP requests from it.
  • We invoke both SendGreetingEmail and InsertGreeting from the workflow.
  • We introduce a sleep allowing you to interrupt the program midway through the workflow.

To see your workflow in action, build and start your application:

npm run build
npx dbos start

Then, visit http://localhost:3000/greeting/Mike in your browser to send a request to your application. On your terminal, you should see an output like:

> npx dbos start
[info]: Workflow executor initialized
[info]: HTTP endpoints supported:
[info]: GET : /greeting/:friend
[info]: Kafka endpoints supported:
[info]: Scheduled endpoints:
[info]: DBOS Server is running at http://localhost:3000
[info]: DBOS Admin Server is running at http://localhost:3001
[info]: Sending email "Thank you for being awesome, Mike!" to Mike...
[info]: Email sent!
[info]: Press Control + C to interrupt the workflow...

Press Control + C when prompted to interrupt your application. Then, run npx dbos start to restart your application. You should see an output like:

> npx dbos start
[info]: Workflow executor initialized
[info]: HTTP endpoints supported:
[info]: GET : /greeting/:friend
[info]: Kafka endpoints supported:
[info]: Scheduled endpoints:
[info]: DBOS Server is running at http://localhost:3000
[info]: DBOS Admin Server is running at http://localhost:3001
[info]: Press Control + C to interrupt the workflow...
[info]: Press Control + C to interrupt the workflow...
[info]: Press Control + C to interrupt the workflow...
[info]: Press Control + C to interrupt the workflow...
[info]: Press Control + C to interrupt the workflow...
[info]: Greeting to Mike recorded in the database!

Notice how DBOS automatically resumes your workflow from where it left off. It doesn't re-send the greeting email, but does record the previously-sent greeting in the database. This reliability is a core feature of DBOS: workflows always run to completion and each of their operations executes exactly once. To learn more about workflows, check out our tutorial and explainer.

The code for this guide is available on GitHub.

Next, to learn how to build more complex applications, check out our tutorials. To walk through a more complex workflow, visit our checkout workflow tutorial.