Learn DBOS TypeScript
This tutorial assumes you have finished the Quickstart and you have a DBOS Transact app running locally. In this guide we'll modify that example to reliably record events across two different systems: Postgres and a third-party API. This app will write to both systems consistently, even if interrupted or restarted at any point.
1. Serving HTTP Requests
Let's start with a simple HTTP GET handler to greet friends. In your app folder, change the file src/operations.ts
to contain only the following:
import { HandlerContext, GetApi } from '@dbos-inc/dbos-sdk';
export class Greetings {
@GetApi('/') // Serve a quick readme for the app
static async readme(_ctxt: HandlerContext) {
const readme = '<html><body><p>' +
'Welcome! Visit the route /greeting/:name to be greeted!<br>' +
'For example, visit <a href="/greeting/dbos">/greeting/dbos</a>.<br>' +
'</p></body></html>';
return Promise.resolve(readme);
}
@GetApi('/greeting/:friend')
static async Greeting(ctxt: HandlerContext, friend: string) {
return Promise.resolve(`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. To learn more about HTTP serving in DBOS, see our HTTP Serving Tutorial.
2. Creating Database Tables
Let's make a database table to record greetings. In DBOS, we recommend managing database tables using schema migrations. By default, we use Knex. We also support Drizzle, 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 replace the contents with the following:
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: name
and note
. Run it like so:
npx dbos migrate
This command should print Migration successful!
3. Writing to the Database
Now that we have greetings
table, let's change our app to write to it. We'll do this with a transactional function. Change your src/operations.ts
to contain:
import { TransactionContext, Transaction, HandlerContext, GetApi } from '@dbos-inc/dbos-sdk';
import { Knex } from 'knex';
interface GreetingRecord {
name: string;
note: string;
}
export class Greetings {
//Omitted for brevity: @GetApi('/') //app readme
@Transaction()
static async InsertGreeting(ctxt: TransactionContext<Knex>, gr: GreetingRecord) {
await ctxt.client('greetings').insert(gr);
ctxt.logger.info(`Greeting to ${gr.name} 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(
{ name: friend, note: noteContent }
);
return noteContent;
}
}
Here we define a GreetingRecord
interface matching a row of data in our greetings
table. We then define a @Transaction
called InsertGreeting
that inserts a new GreetingRecord
into greetings
. Finally, we add a line to the GET API function Greeting
to invoke InsertGreeting
with the provided name
and a welcoming note
.
In this quickstart, we run queries using the Knex query builder. DBOS Transact also supports Drizzle, TypeORM, Prisma, and Raw SQL.
Stop your app with CTRL+C. Rebuild with npm run build
and start with npx dbos start
. Make a few visits to the greeting URL in your browser, i.e. http://localhost:3000/greeting/Mike. With every new visit, the app should print this to the console:
[info]: Greeting to Mike recorded in the database!
3.1. Reading from the Database
You can add another GET API function to read all the greetings from the database like so:
//export class Greetings {
//...
@Transaction({readOnly: true})
@GetApi('/greetings')
static async allGreetings(ctxt: TransactionContext<Knex>) {
return await ctxt.client<GreetingRecord>('greetings').select('*');
}
//}
Here we use @GetApi
and @Transaction
together. This transaction only reads data so we mark it as {readOnly: true}
. This enables DBOS to execute it faster, with fewer database round-trips.
4. Interacting with External Services
Now suppose we also want to send our greetings to a remote system. In this example, we'll use a demo DBOS Guestbook app. It lets us generate an API key and use it to record greetings in an online guestbook.
4.1. Create a Guestbook Key
To generate a guestbook API key, visit https://demo-guestbook.cloud.dbos.dev/key. It should output a 36-character sequence like 12345abc-1234-5678-1234-567890abcdef
(yours will be different).
You can pass this key to your app as a config variable. In your app folder, edit the file dbos-config.yaml
. Add a new env:
section at the bottom with the variable GUESTBOOK_KEY
set to your key in quotes:
env:
GUESTBOOK_KEY: 'your-key-value-here'
For example, if your key is 12345abc-1234-5678-1234-567890abcdef
then you should add:
env:
GUESTBOOK_KEY: '12345abc-1234-5678-1234-567890abcdef'
In production, we recommend storing API keys and other secrets in an environment variable instead of plaintext. To do this, change the configuration to GUESTBOOK_KEY: ${ENV_GUESTBOOK_KEY}
and set ENV_GUESTBOOK_KEY
in your environment prior to starting or deploying the app. See the Configuration Guide for more details.
4.2. Sign the Guestbook from the App
Let's update our app to record each greeting in the guestbook.
In DBOS, we strongly recommend wrapping all such calls to third-party APIs in Steps.
Change your src/operations.ts
to contain the following:
import {
TransactionContext, Transaction, HandlerContext, GetApi,
StepContext, Step, DBOSResponseError
} from "@dbos-inc/dbos-sdk";
import { Knex } from "knex";
interface GreetingRecord {
name: string;
note: string;
}
export class Greetings {
//Omitted for brevity: @GetApi('/') //app readme
//Omitted for brevity: @GetApi('/greetings') //read greetings from database
@Step()
static async SignGuestbook(ctxt: StepContext, name: string) {
const response = await fetch('https://demo-guestbook.cloud.dbos.dev/record_greeting', {
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({ 'key': process.env.GUESTBOOK_KEY, 'name': name})
});
const responseStr = JSON.stringify(await response.json());
if (!response.ok) {
throw new DBOSResponseError(responseStr);
}
ctxt.logger.info(`>>> STEP 1: Signed the Guestbook: ${responseStr}`);
}
@Transaction()
static async InsertGreeting(ctxt: TransactionContext<Knex>, gr: GreetingRecord) {
await ctxt.client('greetings').insert(gr);
ctxt.logger.info(`>>> STEP 2: Greeting to ${gr.name} 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).SignGuestbook(friend);
await ctxt.invoke(Greetings).InsertGreeting(
{ name: friend, note: noteContent }
);
return noteContent;
}
}
We add a new @Step
function called SignGuestbook
that uses fetch
to send an HTTP POST request to the guestbook to record a greeting. If the step throws an error, it is automatically retried up to 3 times with exponential backoff. This is configurable via the StepConfig
parameter to @Step
.
Stop your app with CTRL+C, rebuild with npm run build
and start your application with npx dbos start
. Make a few visits to the greeting URL in your browser, i.e. http://localhost:3000/greeting/Mike. With every new visit, the app should now print first that it has recorded your greeting in the guestbook, then that it has recorded your greeting in the database.
[info]: >>> STEP 1: Signed the Guestbook: {"ip_address":"...","greeted_name":"Mike","greeted_ts":"..."}
[info]: >>> STEP 2: Greeting to Mike recorded in the database!
You can visit the URL https://demo-guestbook.cloud.dbos.dev/greetings/your-key-value
to see all the Guestbook greetings made with your key. Old greetings and keys are removed after a few days.
5. Composing Reliable Workflows
Next, we want to make our app reliable: guarantee that it inserts exactly one database record per guestbook signature, even if interrupted or restarted. DBOS makes this easy with workflows. To see them in action, change your src/operations.ts
like so:
import {
TransactionContext, Transaction, StepContext, Step,
WorkflowContext, Workflow, GetApi, HandlerContext, DBOSResponseError
} from "@dbos-inc/dbos-sdk";
import { Knex } from "knex";
interface GreetingRecord {
name: string;
note: string;
}
export class Greetings {
//Omitted for brevity: @GetApi('/') //app readme
//Omitted for brevity: @GetApi('/greetings') //read greetings from database
@Step()
static async SignGuestbook(ctxt: StepContext, name: string) {
const response = await fetch('https://demo-guestbook.cloud.dbos.dev/record_greeting', {
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify({ 'key': process.env.GUESTBOOK_KEY, 'name': name})
});
const responseStr = JSON.stringify(await response.json());
if (!response.ok) {
throw new DBOSResponseError(responseStr);
}
ctxt.logger.info(`>>> STEP 1: Signed the Guestbook: ${responseStr}`);
}
@Transaction()
static async InsertGreeting(ctxt: TransactionContext<Knex>, gr: GreetingRecord) {
await ctxt.client('greetings').insert(gr);
ctxt.logger.info(`>>> STEP 2: Greeting to ${gr.name} recorded in the database!`);
}
@Workflow()
static async GreetingWorkflow(ctxt: WorkflowContext, friend: string, noteContent: string) {
await ctxt.invoke(Greetings).SignGuestbook(friend);
for (let i = 0; i < 5; i++) {
ctxt.logger.info("Press Control + C to stop the app...");
await ctxt.sleepms(1000);
}
await ctxt.invoke(Greetings).InsertGreeting(
{ name: friend, note: noteContent }
);
}
@GetApi('/greeting/:friend')
static async Greeting(ctxt: HandlerContext, friend: string) {
const noteContent = `Thank you for being awesome, ${friend}!`;
await ctxt.startWorkflow(Greetings).GreetingWorkflow(friend, noteContent);
return Promise.resolve(noteContent);
}
}
Here we create a @Workflow
function called GreetingWorkflow
that invokes SignGuestbook
and then InsertGreeting
. We introduce a sleep between them allowing you to interrupt the program midway through the workflow. We then change Greeting
to start this workflow. Stop your app with CTRL+C, rebuild with npm run build
and start your application with npx dbos start
.
The next step is time-sensitive; you may want to read it over before running. First, visit http://localhost:3000/greeting/Mike in your browser to send a request to your application. In 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]: >>> STEP 1: Signed the Guestbook: {"ip_address":"...","greeted_name":"Mike","greeted_ts":"..."}
[info]: Press Control + C to interrupt the workflow...
[info]: Press Control + C to interrupt the workflow...
Now press Ctrl + C stop your app. Then, run npx dbos start
to restart it. 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]: >>> STEP 2: Greeting to Mike recorded in the database!
If your app did not use a DBOS @Workflow
you would expect it to restart with a "clean slate" and completely forget about your interrupted workflow. However, DBOS automatically resumes your workflow from where it left off and properly completes it by recording the greeting to the database. This reliability is a core feature of DBOS: workflows always continue execution from the last completed step and run to completion. To learn more about workflows, check out our tutorial and explainer.
Here we use startWorkflow
which returns the response to the caller as soon as the workflow starts, without waiting for it to finish. DBOS guarantees that the workflow continues to process to completion. This behavior is preferred when the caller expects a fast response, such as with a payment webhook. To make it synchronous, change startWorkflow
to invokeWorkflow
.
The code for this guide is available on GitHub.
Next, to learn how to build more complex applications, check out our TypeScript tutorials. To walk through a more complex workflow, visit our checkout workflow tutorial.