Skip to main content

Programming Quickstart

Let's learn how to build applications with DBOS Transact, the open-source TypeScript framework for DBOS. In this tutorial, we will modify the example application from our quickstart to reliably send a greeting note to your friends. Along the way, we'll introduce you to core DBOS concepts and show how you can easily build a transactional and reliable application. First, you'll learn to create HTTP endpoints to serve requests. Then, you'll learn how to interact with a database and make third-party API calls from your application. Finally, you'll compose these steps in reliable workflows.

This tutorial assumes you've finished our quickstart. For convenience, we recommend initializing a new application and starting a database for it:

npx -y @dbos-inc/create@latest -n <app-name>
cd <app-name>
export PGPASSWORD=dbos
./start_postgres_docker.sh
npx dbos migrate
truncate -s 0 src/operations.ts

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]: 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.

Connecting to the Database

what you will learn

How to interact with the database.

Let's augment the code to insert a new record in the database when we greet a friend. Using the @Transaction decorator, you can access a managed database client that automatically creates a database connection for you. To try it out, 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, content: string) {
await ctxt.client.raw('INSERT INTO dbos_greetings (greeting_name, greeting_note_content) VALUES (?, ?)', [friend, content]);
}

@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 can access the database.
  • Inside InsertGreeting, we insert a row in the database with ctxt.client.raw().
  • We invoke InsertGreeting from Greeting using its context: ctxt.invoke(Greetings).InsertGreeting(friend, noteContent).

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 we also support knex's query builder and TypeORM.

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, content: string) {
await ctxt.client.raw(
"INSERT INTO dbos_greetings (greeting_name, greeting_note_content) VALUES (?, ?)",
[friend, content],
);
}

@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 invoke SendGreetingEmail from Greeting using its context: ctxt.invoke(Greetings).SendGreetingEmail(friend, noteContent).

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.

To avoid spamming our friends, we want to make sure that if we retry a request after a transient failure or service interruption, the email is sent exactly once. DBOS makes this easy with workflows. To see them in action, add this code to src/operations.ts:

import {
TransactionContext, Transaction,
HandlerContext, 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, content: string) {
await ctxt.client.raw(
"INSERT INTO dbos_greetings (greeting_name, greeting_note_content) VALUES (?, ?)",
[friend, content]
);
}

@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.sleep(1);
}

await ctxt.invoke(Greetings).InsertGreeting(friend, noteContent);
ctxt.logger.info(`Greeting sent to ${friend}!`);
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 this workflow.
  • We introduce a sleep allowing you to interrupt the program midway through the workflow.

When executing a workflow, DBOS persists the output of each step in your database. That way, if a workflow is interrupted, DBOS can restart it from where it left off. To see this in action, build and start the application by running:

npm run build
npx dbos start

Then, visit http://localhost:3000/greeting/Mike in your browser to send a request to the 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]: 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 the workflow. Then, run npx dbos start to restart DBOS Cloud. You should see an output like:

> npx dbos start
[info]: Workflow executor initialized
[info]: HTTP endpoints supported:
[info]: GET : /greeting/:friend
[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 sent to Mike!

Notice how DBOS automatically restarted your program and ran it to completion, but didn't re-send the email. This reliability is a core feature of DBOS: workflows always run to completion and each of their operations executes once and only once.

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.