Integrating Temporal into an existing Next.js application
This tutorial is a work in progress. Some sections may be incomplete, out of date, or missing. We're working to update it.
Introduction
In this tutorial, you'll explore how Temporal integrates into an existing Next.js application using Next.js API routes. This gives you the ability to write full-stack, long-running applications end to end in TypeScript.
This tutorial is written for a reasonably experienced TypeScript/Next.js developer. Whether you are using Gatsby Functions, Blitz.js API Routes or just have a standard Express.js app, you should be able to adapt this tutorial with only minor modifications.
To skip straight to a fully working example, you can check our samples-typescript repo or use the package initializer to create a new project with the following command:
npx @temporalio/create@latest nextjs-temporal-app --sample nextjs-ecommerce-oneclick
Prerequisites
- Set up a local development environment for developing Temporal applications using TypeScript
- Review the Hello World in TypeScript tutorial to understand the basics of getting a Temporal TypeScript SDK project up and running.
Add Temporal to your Next.js project
Temporal doesn't prescribe folder structure; feel free to ignore or modify these instructions per your own needs.
You can install Temporal's packages with a single dependency, then set up folders and files for your Workflow, Activity, and Worker code:
npm i @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity # in Next.js project root
mkdir -p temporal/src # create folder, recursively
cd temporal
touch src/worker.ts src/workflows.ts src/activities.ts
Configure TypeScript to compile from temporal/src
to temporal/lib
with a tsconfig.json
.
Sample tsconfig.json
to get you started:
// /temporal/tsconfig.json
{
"extends": "@tsconfig/node16/tsconfig.json", // optional but nice to have
"version": "4.4.2",
"compilerOptions": {
"emitDecoratorMetadata": false,
"experimentalDecorators": false,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"rootDir": "./src",
"outDir": "./lib"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
For convenience, you may want to set up some npm scripts to run the builds in your project root package.json
.
// /package.json
"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": "npm run dev",
"start:worker": "nodemon ./temporal/lib/worker",
"lint": "eslint ."
},
In the above example we use npm-run-all
and nodemon
so that we are able to do 4 things:
- build Temporal once
- start Next.js locally
- start a Temporal Worker
- rebuild Temporal files on change
in a single npm run dev
command.
Write your first Workflow, Activity and Worker
Inside of /temporal/src/activities.ts
we'll write a simple Activity function to start with:
- TypeScript
- JavaScript
// /temporal/src/activities.ts
import { Context } from '@temporalio/activity';
export async function purchase(id: string): Promise<string> {
console.log(`Purchased ${id}!`);
return Context.current().info.activityId;
}
// /temporal/src/activities.ts
import { Context } from '@temporalio/activity';
export async function purchase(id) {
console.log(`Purchased ${id}!`);
return Context.current().info.activityId;
}
Activities are the only way to interact with the outside world in Temporal (e.g. making API requests, or accessing the filesystem). See the Activities docs for more info.
Inside of /temporal/src/workflows.ts
we'll write a Workflow function that calls this Activity:
- TypeScript
- JavaScript
// /temporal/src/workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow';
import type * as activities from './activities'; // purely for type safety
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'); // demo use of timer
console.log(`Activity ID: ${result} executed!`);
}
// /temporal/src/workflows.ts
import { proxyActivities, sleep } from '@temporalio/workflow';
const { purchase } = proxyActivities({
startToCloseTimeout: '1 minute',
});
export async function OneClickBuy(id) {
const result = await purchase(id); // calling the activity
await sleep('10 seconds'); // demo use of timer
console.log(`Activity ID: ${result} executed!`);
}
Workflow code is bundled and run inside a deterministic v8 isolate so we can persist and replay every state change.
This is why Workflow code must be separate from Activity code, and why we have to proxyActivities
instead of directly importing them.
Workflows also have access to a special set of Workflow APIs which we recommend exploring next.
With your Workflows and Activities done, you can now write the Worker that will host both and poll the tutorial
Task Queue:
- TypeScript
- JavaScript
// /temporal/src/worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
run().catch((err) => console.log(err));
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'), // passed to Webpack for bundling
activities, // directly imported in Node.js
taskQueue: 'tutorial',
});
await worker.run();
}
// /temporal/src/worker.ts
import { Worker } from '@temporalio/worker';
import * as activities from './activities';
run().catch((err) => console.log(err));
async function run() {
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'tutorial',
});
await worker.run();
}
See the full Worker docs for more info.
You should now be able to run your Worker with npm run build:temporal && npm run start:worker
, but it's not very exciting because you have no way to start a Workflow yet.
You actually can start a Workflow with tctl
with just a Worker running, and no Client code written!
It is out of scope for this tutorial but try to brew install tctl
and then tctl workflow run --tq tutorial --wt OneClickBuy --et 60 -i '"Temporal CLI"'
if you enjoy developing with CLIs.
Write a Temporal Client inside a Next.js API Route
We will use Next.js API routes to expose a serverless endpoint that can be called by our frontend and then communicate with Temporal on the backend:
# in Next.js project root
mkdir pages/api
touch pages/api/startBuy.ts
Now we will create a Client and start a Workflow Execution:
- TypeScript
- JavaScript
// pages/api/startBuy.ts
import { Connection, WorkflowClient } from '@temporalio/client';
import { OneClickBuy } from '../../temporal/lib/workflows.js';
export default async function startBuy(req, res) {
const { itemId } = req.body; // TODO: validate itemId and req.method
const client = new WorkflowClient();
const handle = await client.start(OneClickBuy, {
workflowId: 'business-meaningful-id',
taskQueue: 'tutorial', // must match the taskQueue polled by Worker above
args: [itemId],
// workflowId: // TODO: use business-meaningful user/transaction ID here
}); // kick off the purchase async
res.status(200).json({ workflowId: handle.workflowId });
}
// pages/api/startBuy.ts
import { WorkflowClient } from '@temporalio/client';
import { OneClickBuy } from '../../temporal/lib/workflows.js';
export default async function startBuy(req, res) {
const { itemId } = req.body; // TODO: validate itemId and req.method
const client = new WorkflowClient();
const handle = await client.start(OneClickBuy, {
workflowId: 'business-meaningful-id',
taskQueue: 'tutorial',
args: [itemId],
// workflowId: // TODO: use business-meaningful user/transaction ID here
}); // kick off the purchase async
res.status(200).json({ workflowId: handle.workflowId });
}
Now if you have Next.js and Temporal running, you can at least start a Workflow Execution:
npm run dev # start Temporal and Next.js in parallel
curl -d '{"itemId":"item123"}' -H "Content-Type: application/json" -X POST http://localhost:3000/api/startBuy
The terminal that has your Temporal Worker will print Purchased item123
if everything is working properly.
Call the API Route from the Next.js frontend
If you are an experienced React/Next.js dev you should know what to do here.
For tutorial purposes we will just assume you have an itemId
to use here; in real life you are likely to pull this from some other data source like Shopify or a database.
- TypeScript
- JavaScript
// /pages/index.ts or whatever page you are on
// inside event handler
fetch('/api/startBuy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemId }),
});
// /pages/index.ts or whatever page you are on
// inside event handler
fetch('/api/startBuy', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemId }),
});
We recommend tracking the state of this API call and possibly toasting success, per our sample code, but of course it is up to you what UX you want to provide.
Deploying your Temporal + Next.js app
Your Next.js app, including Next.js API Routes with Temporal Clients in them, can be deployed anywhere Next.js can be deployed, including in serverless environments like Vercel or Netlify.
However, your Temporal Workers must be deployed in traditional "serverful" environments (e.g. with EC2, Digital Ocean or Render, not a serverless environment).
Both Temporal Clients and Temporal Workers must be configured to communicate with a Temporal Server instance, whether self-hosted or Temporal Cloud. You will need to configure gRPC connection address, namespace, and mTLS cert and key (strongly recommended).
- TypeScript
- JavaScript
// before Worker.create call in worker.ts
const connection = await NativeConnection.connect({
address,
tls: {
serverNameOverride,
serverRootCACertificate,
clientCertPair: {
crt: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
},
},
});
// inside each Client call inside API Route
const connection = await Connection.connect({
address,
tls: {
serverNameOverride,
serverRootCACertificate,
clientCertPair: {
crt: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
},
},
});
// before Worker.create call in worker.ts
const connection = await NativeConnection.connect({
address,
tls: {
serverNameOverride,
serverRootCACertificate,
clientCertPair: {
crt: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
},
},
});
// inside each Client call inside API Route
const connection = await Connection.connect({
address,
tls: {
serverNameOverride,
serverRootCACertificate,
clientCertPair: {
crt: fs.readFileSync(clientCertPath),
key: fs.readFileSync(clientKeyPath),
},
},
});
See the mTLS tutorial for full details, or get in touch with us on Slack if you have reached this stage.
Production Concerns
As you move into production with your app, please review our docs on:
- Securing
- Testing
- Patching (aka migrating code to new versions)
- Logging
- Production Deploy Checklist
You will also want to have a plan for monitoring and scaling your Temporal Workers that host and execute your Activity and Workflow code (separately from monitoring and scaling Temporal Server itself).
Conclusion
At this point, you have a working full stack example of a Temporal Workflow running inside your Next.js app.
You can explore adding Signals Queries to your Workflow, then adding a new API Route to call them. You can choose to set up one API Route per Signal or Query, or have one API Route handle all of them, Temporal has no opinion on how you set up routing.
Again, for a fully working example, you can check our samples-typescript repo.