Next.js Integration

BAML can be used with Vercel’s AI SDK to stream BAML functions to your UI.

The latest example code is found in our NextJS starter, but this tutorial will guide you on how to add BAML step-by-step.

See the live demo

You will need to use Server Actions, from the App Router, for this tutorial. You can still stream BAML functions from Route Handlers however.

1

Install BAML, and Generate a BAML client for TypeScript

  • Follow the TS installation guide
  • Install the VSCode extension and Save a baml file to generate the client (or use npx baml-cli generate).
2

Update next.config.mjs

@boundaryml/baml uses a native node addon to run the BAML functions. You need to tell NextJS to use the loader for these files.

1/** @type {import('next').NextConfig} */
2const nextConfig = {
3 experimental: {
4 serverComponentsExternalPackages: ["@boundaryml/baml"],
5 },
6 webpack: (config, { dev, isServer, webpack, nextRuntime }) => {
7 config.module.rules.push({
8 test: /\.node$/,
9 use: [
10 {
11 loader: "nextjs-node-loader",
12 options: {
13 outputPath: config.output.path,
14 },
15 },
16 ],
17 });
18
19 return config;
20 },
21};
22
23export default nextConfig;
3

Create some helper utilities to stream BAML functions

Let’s add some helpers to export our baml functions as streamable server actions. See the last line in this file, where we export the extractResume function.

In app/utils/streamableObject.tsx add the following code:

1import { createStreamableValue, StreamableValue as BaseStreamableValue } from "ai/rsc";
2import { BamlStream } from "@boundaryml/baml";
3import { b } from "@/baml_client"; // You can change the path of this to wherever your baml_client is located.
4
5
6// ------------------------------
7// Helper functions
8// ------------------------------
9
10/**
11 * Type alias for defining a StreamableValue based on a BamlStream.
12 * It captures either a partial or final result depending on the stream state.
13 */
14type StreamableValue<T extends BamlStream<any, any>> =
15 | { partial: T extends BamlStream<infer StreamRet, any> ? StreamRet : never }
16 | { final: T extends BamlStream<any, infer Ret> ? Ret : never };
17
18/**
19 * Helper function to manage and handle a BamlStream.
20 * It consumes the stream, updates the streamable value for each partial event,
21 * and finalizes the stream when complete.
22 *
23 * @param bamlStream - The BamlStream to be processed.
24 * @returns A promise that resolves with an object containing the BaseStreamableValue.
25 */
26export async function streamHelper<T extends BamlStream<any, any>>(
27 bamlStream: T,
28): Promise<{
29 object: BaseStreamableValue<StreamableValue<T>>;
30}> {
31 const stream = createStreamableValue<StreamableValue<T>>();
32
33 // Asynchronous function to process the BamlStream events
34 (async () => {
35 try {
36 // Iterate through the stream and update the stream value with partial data
37 for await (const event of bamlStream) {
38 stream.update({ partial: event });
39 }
40
41 // Obtain the final response once all events are processed
42 const response = await bamlStream.getFinalResponse();
43 stream.done({ final: response });
44 } catch (err) {
45 // Handle any errors during stream processing
46 stream.error(err);
47 }
48 })();
49
50 return { object: stream.value };
51}
52
53/**
54 * Utility function to create a streamable function from a BamlStream-producing function.
55 * This function returns an asynchronous function that manages the streaming process.
56 *
57 * @param func - A function that produces a BamlStream when called.
58 * @returns An asynchronous function that returns a BaseStreamableValue for the stream.
59 */
60export function makeStreamable<
61 BamlStreamFunc extends (...args: any) => BamlStream<any, any>,
62>(
63 func: BamlStreamFunc
64): (...args: Parameters<BamlStreamFunc>) => Promise<{
65 object: BaseStreamableValue<StreamableValue<ReturnType<BamlStreamFunc>>>;
66}> {
67 return async (...args) => {
68 const boundFunc = func.bind(b.stream);
69 const stream = boundFunc(...args);
70 return streamHelper(stream);
71 };
72}
4

Export your BAML functions to streamable server actions

In app/actions/extract.tsx add the following code:

1import { makeStreamable } from "../_baml_utils/streamableObjects";
2
3
4export const extractResume = makeStreamable(b.stream.ExtractResume);
5

Create a hook to use the streamable functions in React Components

This hook will work like react-query, but for BAML functions. It will give you partial data, the loading status, and whether the stream was completed.

In app/_hooks/useStream.ts add:

1import { useState, useEffect } from "react";
2import { readStreamableValue, StreamableValue } from "ai/rsc";
3
4/**
5 * A hook that streams data from a server action. The server action must return a StreamableValue.
6 * See the example actiimport { useState, useEffect } from "react";
7import { readStreamableValue, StreamableValue } from "ai/rsc";
8
9/**
10 * A hook that streams data from a server action. The server action must return a StreamableValue.
11 * See the example action in app/actions/streamable_objects.tsx
12 * **/
13export function useStream<PartialRet, Ret, P extends any[]>(
14 serverAction: (...args: P) => Promise<{ object: StreamableValue<{ partial: PartialRet } | { final: Ret }, any> }>
15) {
16 const [isLoading, setIsLoading] = useState(false);
17 const [isComplete, setIsComplete] = useState(false);
18 const [isError, setIsError] = useState(false);
19 const [error, setError] = useState<Error | null>(null);
20 const [partialData, setPartialData] = useState<PartialRet | undefined>(undefined); // Initialize data state
21 const [streamResult, setData] = useState<Ret | undefined>(undefined); // full non-partial data
22
23 const mutate = async (
24 ...params: Parameters<typeof serverAction>
25 ): Promise<Ret | undefined> => {
26 console.log("mutate", params);
27 setIsLoading(true);
28 setIsError(false);
29 setError(null);
30
31 try {
32 const { object } = await serverAction(...params);
33 const asyncIterable = readStreamableValue(object);
34
35 for await (const value of asyncIterable) {
36 if (value !== undefined) {
37
38 // could also add a callback here.
39 // if (options?.onData) {
40 // options.onData(value as T);
41 // }
42 console.log("value", value);
43 if ("partial" in value) {
44 setPartialData(value.partial); // Update data state with the latest value
45 } else if ("final" in value) {
46 setData(value.final); // Update data state with the latest value
47 setIsComplete(true);
48 return value.final;
49 }
50 }
51 }
52
53 // // If it completes, it means it's the full data.
54 // return streamedData;
55 } catch (err) {
56 console.log("error", err);
57
58 setIsError(true);
59 setError(new Error(JSON.stringify(err) ?? "An error occurred"));
60 return undefined;
61 } finally {
62 setIsLoading(false);
63 }
64 };
65
66 // If you use the "data" property, your component will re-render when the data gets updated.
67 return { data: streamResult, partialData, isLoading, isComplete, isError, error, mutate };
68}
6

Stream your BAML function in a component

In app/page.tsx you can use the hook to stream the BAML function and render the result in real-time.

1"use client";
2import {
3 extractResume,
4 extractUnstructuredResume,
5} from "../../actions/streamable_objects";
6// import types from baml files like this:
7import { Resume } from "@/baml_client";
8
9export default function Home() {
10 // you can also rename these fields by using ":", like how we renamed partialData to "partialResume"
11 // `mutate` is a function that will start the stream. It takes in the same arguments as the BAML function.
12 const { data: completedData, partialData: partialResume, isLoading, isError, error, mutate } = useStream(extractResume);
13
14 return (
15 <div>
16 <h1>BoundaryML Next.js Example</h1>
17
18 <button onClick={() => mutate("Some resume text")}>Stream BAML</button>
19 {isLoading && <p>Loading...</p>}
20 {isError && <p>Error: {error?.message}</p>}
21 {partialData && <pre>{JSON.stringify(partialData, null, 2)}</pre>}
22 {data && <pre>{JSON.stringify(data, null, 2)}</pre>}
23 </div>
24 );
25}

And now you’re all set!

If you have issues with your environment variables not loading, you may want to use dotenv-cli to load your env vars before the nextjs process starts:

dotenv -- npm run dev