Skip to main content

Build a one-click order application with TypeScript and Next.js

Temporal TypeScript SDK

Introduction

When you're building an e-commerce application, you want to give customers a great user experience. You also need to make sure that any calls to external services, like databases, payment gateways, and other tools, are reliable.

Next.js is a popular choice for building full-stack web applications using Node.js and React. You can deliver a great experience across the stack by integrating a Temporal Workflow with Next.js. Temporal provides fault tolerance and ensures that long-running processes and background tasks complete successfully, even in the event of failures. This is ideal for critical business operations and transactions.

In this tutorial you'll build a back-end API using Nest API Routes that starts a Temporal Workflow. Then you'll build a quick user interface with React and Tailwind to call that API. When you're done, you'll have a framework you can follow for building full-stack web applications powered by Temporal.

Prerequisites

Before starting this tutorial:

Create your project

You'll use the Next.js project generator to create a basic Next.js application. Then you'll configure dependencies for the project and set up a Temporal project within the Next.js application.

Create a new Next.js project with the create-next-app tool. Call the project nextjs-temporal:

npx create-next-app@latest nextjs-temporal

The project generator will prompt you with several questions. Accept the default values for each option. When the project generator completes, and the dependencies install, switch to the new project's root directory:

cd nextjs-temporal

Install the @tsconfig/node20 package as a developer dependency, as you'll use it as the foundation for a Temporal-specific tsconfig file you'll keep separate from the one generated by the Next.js project generator:

npm install --save-dev @tsconfig/node20

Next, install Nodemon which you'll use to watch your files for changes and reload Temporal Workers:

npm install --save-dev nodemon

With the dependencies installed, you can add Temporal to the project.

In the root directory of your Next.js project, execute the following command to install Temporal and its dependencies:

npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity

Now create a directory to hold your Temporal Workflows and Activities:

mkdir -p temporal/src

Configure TypeScript to compile from temporal/src to temporal/lib by adding a new tsconfig.json in the temporal/src folder:

touch temporal/tsconfig.json

Add the following configuration to the file:

{
"extends": "@tsconfig/node20/tsconfig.json",
"version": "4.4.2",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["src/**/*.ts"]
}

For convenience, set up some scripts to run the builds in your project root's package.json.

Add the npm-run-all command to your project as a dependency:

npm install npm-run-all --save-dev

Then use npm-run-all to define scripts to run your Temporal build processes and run the Next.JS app alongside Temporal Workers.

Locate your existing scripts section, which defines scripts to run and build Next.js applications:

  "scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},

Change the scripts so you can build and run your Next.js application and your Temporal Workers at the same time:

  "scripts": {
"dev": "npm-run-all -l build:temporal --parallel dev:temporal dev:next start:worker",
"dev:next": "next dev",
"dev:temporal": "tsc --build --watch ./temporal/tsconfig.json",
"build:next": "next build",
"build:temporal": "tsc --build ./temporal/tsconfig.json",
"start:worker": "nodemon ./temporal/lib/worker",
"start": "next start",
"lint": "eslint ."
},

These scripts let you run the following steps with a single npm run dev command:

  • build Temporal once.
  • start Next.js locally.
  • start a Temporal Worker.
  • rebuild Temporal files when they change.

With your project configured, you can write your Temporal Workflows and Activities.

Define the business logic using Temporal

You'll use Temporal to power a one-click ordering process. You'll use a Temporal Workflow to represent each order.

Workflows define the overall flow of your business process. Conceptually, a Workflow is a sequence of steps written in your programming language.

Workflows orchestrate Activities, which is how you interact with the outside world in Temporal. You use Activities to make API requests, access the file system, or perform other non-deterministic operations.

The Workflow you'll create in this tutorial will have a single Activity, purchase.

Create the file temporal/src/activities.ts:

touch temporal/src/activities.ts

Inside of temporal/src/activities.ts, add the following code to define the purchase function:

temporal/src/activities.ts

import { activityInfo } from '@temporalio/activity';
export async function purchase(id: string): Promise<string> {
console.log(`Purchased ${id}!`);
return activityInfo().activityId;
}

For this tutorial, the function prints a message to the console and returns the ID of the Activity. In a real application, this function would interact with a payment API and attempt to make a payment. You may also have other Activities that send emails or persist things to databases.

Now define the oneClickBuy Workflow that calls this Activity. This is the Workflow you'll invoke from the Next.js API. Create the file temporal/src/workflows.ts:

touch temporal/src/workflows.ts

Add the following code to temporal/src/workflows.ts to define the Workflow:

temporal/src/workflows.ts

import { proxyActivities, sleep } from '@temporalio/workflow';
import type * as activities from './activities';

const { purchase } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
});

export async function oneClickBuy(id: string): Promise<string> {
const result = await purchase(id); // calling the activity
await sleep('10 seconds'); // sleep to simulate a longer response.
console.log(`Activity ID: ${result} executed!`);
return result;
}

This Workflow calls the purchase Activity and then uses await sleep() to create an artificial delay in the Workflow. A more complex Workflow would call more Activities.

Workflows must be deterministic, so you perform non-deterministic work in Activities. The TypeScript SDK bundles Workflow code and runs it inside a deterministic sandbox. This sandbox can help detect if you're using nondeterministic code. This is why you must separate Workflow code from Activity code, and why you have to use the proxyActivities function to load your Activity functions instead of directly importing them. The Activities will be nondeterministic, so you can't run in the same sandbox as the Workflow code.

With your Workflows and Activities in place, you can now write a Worker Program. You use a Worker Program to define a Worker which hosts your Activities and Workflows and polls the ecommerce-oneclick Task Queue to look for work to do.

You'll need to use the Task Queue name in your Worker Program, and any time you interact with a Workflow from your Next.js application. To guarantee you use it consistently, create a constant.

Create the file temporal/src/shared.ts:

touch temporal/src/shared.ts

Add the following line to the file to define the constant:

temporal/src/shared.ts

export const TASK_QUEUE_NAME = 'ecommerce-oneclick';

Now define the Worker program. Create temporal/src/worker.ts:

touch temporal/src/worker.ts

In temporal/src/worker.ts, define the Worker Program:

temporal/src/worker.ts

import { NativeConnection, Worker } from '@temporalio/worker';
import * as activities from './activities';
import { TASK_QUEUE_NAME } from './shared';

run().catch((err) => console.log(err));

async function run() {
const connection = await NativeConnection.connect({
address: 'localhost:7233',
// In production, pass options to configure TLS and other settings.
});
try {
const worker = await Worker.create({
connection,
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: TASK_QUEUE_NAME
});
await worker.run();
} finally {
connection.close();
}
}

When the Worker finds Workflow Tasks or Activity Tasks for the Workflows and Activities it hosts, it executes them.

Run your Worker with the following command to make sure that everything builds and there are no errors:

npm run build:temporal && npm run start:worker`

The Worker runs, but it won't have any tasks to perform because you haven't started a Workflow yet.

Next you'll make a Next.js API route that starts a Temporal Workflow.

Define the back-end API

You'll use Next.js API routes to expose a serverless endpoint that your frontend can call and then communicate with Temporal on the back-end.

You don't want to create a new Temporal Client on every API request, so create a function that creates a new Temporal Client or returns the existing one. In temporal/src add a new file called client.ts:

touch temporal/src/client.ts

Add the following code to temporal/src/client.ts to define a makeClient function that creates the client, and a getTemporalClient function that retrieves it:

temporal/src/client.ts

import { Client, Connection } from '@temporalio/client';

const client: Client = makeClient();

function makeClient(): Client {
const connection = Connection.lazy({
address: 'localhost:7233',
// In production, pass options to configure TLS and other settings.
});
return new Client({ connection });
}

export function getTemporalClient(): Client {
return client;
}

You'll use the getTemporalClient function in your Next.js application any time you need to interact with Temporal.

Now build out the API route to buy an item. In the root of your application, add a new app/api/startBuy folder:

mkdir -p app/api/startBuy

Then create the file app/api/startBuy/route.ts:

touch app/api/startBuy/route.ts

Within the file, create a POST route that fetches a Temporal Client and uses it to start a Workflow Execution:

app/api/startBuy/route.ts

import { oneClickBuy } from '../../../temporal/src/workflows';
import { getTemporalClient } from '../../../temporal/src/client';
import { TASK_QUEUE_NAME } from '../../../temporal/src/shared';

export async function POST(req: Request) {
interface RequestBody {
itemId: string;
transactionId: string;
}

let body: RequestBody;

try {
body = await req.json() as RequestBody;
} catch (error) {
return new Response("Invalid JSON body", { status: 400 });
}

const { itemId, transactionId } = body;

if (!itemId) {
return new Response("Must send the itemID to buy", { status: 400 });
}

await getTemporalClient().workflow.start(oneClickBuy, {
taskQueue: TASK_QUEUE_NAME,
workflowId: transactionId,
args: [itemId],
});

return Response.json({ ok: true });
}

The API route checks the JSON input and ensures a product ID exists. If it does, it then uses the Temporal Client to start the Workflow:

workflow.start sends a request to the Temporal Service to start a Workflow Execution. The actual Workflow doesn't run until a Worker sees the Workflow Task in the Task Queue and starts working on it. This API endpoint immediately returns a response, even though the Workflow has a 10-second delay. If you change this to workflow.execute, the call will block until the Workflow finishes. That's because workflow.start resolves when the Temporal Service acknowledges that it's scheduled the Workflow Execution, whereas workflow.execute resolves only when the Workflow completes.

Make sure you've saved all your changes.

Now start Next.js and the Temporal Worker using the npm run dev script you defined:

npm run dev
Connection issues

If you receive an error like the following:

[start:worker] TransportError: tonic::transport::Error(Transport, hyper::Error(Connect,
ConnectError("tcp connect error", Os { code: 61,
kind: ConnectionRefused, message: "Connection refused" })))

This means the Temporal Client can't connect to the Temporal Service. Make sure you have a local Temporal Service running. Open a separate terminal window and start the service with temporal server start-dev.

In another terminal window, use the curl command to make a request to the API endpoint, which starts the Temporal Workflow:

curl -d '{"itemId":"1", "transactionId":"abc123"}' \
-H "Content-Type: application/json" \
-X POST http://localhost:3000/api/startBuy

The terminal that's running your application and Temporal Worker will print Purchased 1:

[dev:next    ]  POST /api/startBuy 200 in 16ms
[start:worker] Purchased 1!

Remember, because the API endpoint uses workflow.start, it's not blocking on the Workflow Execution itself, so you see an almost immediate response from the API.

Now that you know the API can start Temporal Workflows, you can build the user interface.

Build the front-end interface

You can call your Next.js API with curl, but that's not the user experience you want to present to shoppers. You'll use React components with Next.js to make a request to the API you just created to create the one-click buy experience.

To call the API Route from the Next.js frontend, you'll use the fetch API to make a request to the /api/startbuy route when you click a button.

Build out the front-end user interface. Open app/page.tsx and remove the contents of the file that the project generator created. Then at the top, add the following code to add directives and import the necessary libraries:

app/page.tsx

'use client'
import Head from 'next/head';
import React, { useState, useRef } from 'react';
import { v4 as uuid4 } from 'uuid';

Next, define a TypeScript Interface for the Product's properties, a Type to define states for the purchase, and a collection of Products:

app/page.tsx


interface ProductProps {
product: {
id: number;
name: string;
price: string;
};
}

const products = [
{
id: 1,
name: 'PDF Book',
price: '$49',
},
{
id: 2,
name: 'Kindle Book',
price: '$49',
},
];

type ITEMSTATE = 'NEW' | 'ORDERING' | 'ORDERED' | 'ERROR';

In a production application, your products would come from another part of your application, such as a database or a Shopify store.

Next, define a Product component with the following code:

app/page.tsx

const Product: React.FC<ProductProps> = ({ product }) => {
const itemId = product.id;
const [state, setState] = useState<ITEMSTATE>('NEW');
const [transactionId, setTransactionId] = React.useState(uuid4());

const buyProduct = () => {
setState('ORDERING');
fetch('/api/startBuy', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ itemId, transactionId }),
})
.then(() => {
setState('ORDERED');
})
.catch(() => {
setState('ERROR');
});
};

const buyStyle = "w-full bg-white hover:bg-blue-200 bg-opacity-75 backdrop-filter backdrop-blur py-2 px-4 rounded-md text-sm font-medium text-gray-900 text-center";
const orderingStyle = "w-full bg-yellow-500 bg-opacity-75 backdrop-filter backdrop-blur py-2 px-4 rounded-md text-sm font-medium text-gray-900 text-center";
const orderStyle = "w-full bg-green-500 bg-opacity-75 backdrop-filter backdrop-blur py-2 px-4 rounded-md text-sm font-medium text-gray-900 text-center";
const errorStyle = "w-full bg-white hover:bg-blue-200 bg-opacity-75 backdrop-filter backdrop-blur py-2 px-4 rounded-md text-sm font-medium text-gray-900 text-center";

return (
<div key={product.id} className="relative group">
<div className="mt-4 flex items-center justify-between text-base font-medium text-gray-900 space-x-8">
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
<div className="aspect-w-4 aspect-h-3 rounded-lg overflow-hidden bg-gray-100">
<div className="flex items-end p-4" aria-hidden="true">
{
{
NEW: ( <button onClick={buyProduct} className={buyStyle}> Buy Now </button> ),
ORDERING: ( <div className={orderingStyle}>Orderering</div> ),
ORDERED: ( <div className={orderStyle}>Ordered</div> ),
ERROR: ( <button onClick={buyProduct} className={errorStyle}>Error! Click to Retry </button> ),
}[state]
}
</div>
</div>
</div>
);
};

In this component, the buyProduct function makes the call to start the Temporal Workflow by making a request to the API route you defined. The component renders the product and displays a button for the customer to click. Based on the order state, the component replaces the button with confirmation message or a new button that lets the customer try again if there's an error. Tailwind styles help control how the buttons and messages look.

Now add a ProductList component with the following code that renders each Product component in the list of products:

app/page.tsx

const ProductList: React.FC = () => {
return (
<div className="bg-white">
<div className="max-w-2xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:max-w-7xl lg:px-8">
<div className="mt-6 grid grid-cols-1 gap-x-8 gap-y-8 sm:grid-cols-2 sm:gap-y-10 md:grid-cols-4">
{products.map((product) => (
<Product product={product} key={product.id} />
))}
</div>
</div>
</div>
);
};

Like the Product component. you're using Tailwind to style the product list.

Finally, add the Home component to define the overall page structure and render the product list:

app/page.tsx

const Home: React.FC = () => {
return (
<div className="pt-8 pb-80 sm:pt-12 sm:pb-40 lg:pt-24 lg:pb-48">
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 sm:static">
<Head>
<title>Temporal + Next.js One-Click Purchase</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header className="relative overflow-hidden">
<div className="sm:max-w-lg">
<h1 className="text-4xl font font-extrabold tracking-tight text-gray-900 sm:text-6xl">
Temporal.io + Next.js One Click Purchase
</h1>
<p className="mt-4 text-xl text-gray-500">
Click on the item to buy it now.
</p>
</div>
</header>
<ProductList />
</div>
</div>
);
};

export default Home;

Ensure you've saved all your work.

Visit http://localhost:3000 in your browser and you'll see two products. Click their buttons and you'll execute the Temporal Workflows. The UI state will change almost immediately because the Next.js API route returns immediately, even though you've added an artificial delay to the Workflow. Log into the local Temporal Web UI running on http://localhost:8233 and you'll see the entire Workflow Execution.

You now have a Next.js application that uses Temporal Workflows to power one of its back-end processes.

When you're moving from your local machine to either Temporal Cloud or a self-hosted Temporal Service, you'll need to configure Temporal Clients and Temporal Workers to communicate with the remote service. To connect to a Temporal Service using mTLS authentication, you will need to configure the connection address, Namespace, and mTLS certificates and keys.

Review Run Workers with Temporal Cloud for the TypeScript SDK for more details on creating certificates and connecting to Temporal Cloud.

In this application, you'd change temporal/src/worker.ts to add certificate information:

const connection = await NativeConnection.connect({
address,
tls: {
clientCertPair: {
crt: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
},
},
});

Then you'd update the makeClient function in temporal/src/client.ts to include the same keys:

function makeClient(): Client {
const connection = Connection.lazy({
address: 'localhost:7233',
tls: {
clientCertPair: {
crt: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
},
},
});
return new Client({ connection });
}

Restart your application to establish the connections to the new Temporal Service.

Conclusion

At this point, you have a working full stack example of a Temporal Workflow running inside your Next.js app, and the beginnings of an order processing system. From here you can add more Activities to the Workflow, or use this project as the basis for a different kind of application that needs long-running processes.

As you build out more features, you may find you need to change the state of an in-progress Workflow, or retrieve information from that Workflow. You can use Signals to send asynchronous data to running Workflows, and you can use Queries to check the state of a Workflow. You can map Signals and Queries to new Next.js API routes using the Temporal Client.

For a more detailed example, look at the Next.js E-Commerce One-Click example in the samples-TypeScript repository.

You can deploy your Next.js app, including Next.js API Routes with Temporal Clients in them, anywhere you can deploy Next.js applications. This includes serverless environments like Vercel or Netlify. However, you must deploy your Temporal Workers in traditional environments, such as EC2, DigitalOcean, or Render. They won't work in a serverless environment.

As you move into production with your app, you'll find the following documentation topics helpful: