diff --git a/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx index 559930830e..87f95b70e5 100644 --- a/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx @@ -72,7 +72,9 @@ The response of this API route has the status code `201`. To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. -For example, to create an API route that returns an event stream: +### Example: Server-Sent Events (SSE) + +For example, to create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`: export const streamHighlights = [ ["7", "writeHead", "Set the response's headers."], @@ -97,9 +99,14 @@ export const GET = async ( }) const interval = setInterval(() => { - res.write("Streaming data...\n") + res.write("data: Streaming data...\n\n") }, 3000) + req.on("close", () => { + clearInterval(interval) + res.end() + }) + req.on("end", () => { clearInterval(interval) res.end() @@ -114,6 +121,12 @@ The `writeHead` method accepts two parameters: This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. +### Tip: Fetching Stream with JS SDK + +The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream. + +Learn more in the [JS SDK](!resources!/js-sdk) documentation. + --- ## Do More with Responses diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index d78904b0d7..dc61ef08ff 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -57,7 +57,7 @@ export const generatedEditDates = { "app/learn/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z", "app/learn/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z", "app/learn/fundamentals/modules/service-constraints/page.mdx": "2025-03-18T15:12:46.006Z", - "app/learn/fundamentals/api-routes/responses/page.mdx": "2025-07-25T15:17:39.986Z", + "app/learn/fundamentals/api-routes/responses/page.mdx": "2025-08-01T14:15:34.787Z", "app/learn/fundamentals/api-routes/validation/page.mdx": "2025-03-24T06:52:47.896Z", "app/learn/fundamentals/api-routes/errors/page.mdx": "2025-06-19T16:09:08.563Z", "app/learn/fundamentals/admin/constraints/page.mdx": "2025-07-21T08:20:43.223Z", diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index 8e1afeda0e..29e232fbde 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -8685,7 +8685,9 @@ The response of this API route has the status code `201`. To return response data other than a JSON object, use the `writeHead` method of the `MedusaResponse` object. It allows you to set the response headers, including the content type. -For example, to create an API route that returns an event stream: +### Example: Server-Sent Events (SSE) + +For example, to create an API route that returns server-sent events (SSE), you can set the `Content-Type` header to `text/event-stream`: ```ts highlights={streamHighlights} import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" @@ -8701,9 +8703,14 @@ export const GET = async ( }) const interval = setInterval(() => { - res.write("Streaming data...\n") + res.write("data: Streaming data...\n\n") }, 3000) + req.on("close", () => { + clearInterval(interval) + res.end() + }) + req.on("end", () => { clearInterval(interval) res.end() @@ -8718,6 +8725,12 @@ The `writeHead` method accepts two parameters: This API route opens a stream by setting the `Content-Type` to `text/event-stream`. It then simulates a stream by creating an interval that writes the stream data every three seconds. +### Tip: Fetching Stream with JS SDK + +The JS SDK has a `fetchStream` method that you can use to fetch data from an API route that returns a stream. + +Learn more in the [JS SDK](https://docs.medusajs.com/resources/js-sdk/index.html.md) documentation. + *** ## Do More with Responses @@ -25026,7 +25039,7 @@ Users of any actor type (admin, customer, or custom actor type) can request to r In this guide, you'll implement a subscriber that handles the `auth.password_reset` event to send an email notification to the user with instructions on how to reset their password. -After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa admin, storefront, or API routes. +After adding the subscriber, you will have a complete reset password flow you can utilize using the Medusa Admin, storefront, or API routes. *** @@ -43706,6 +43719,151 @@ The method returns a Promise that, when resolved, has the data returned by the r *** +## Stream Server-Sent Events + +The JS SDK supports streaming server-sent events (SSE) using the `client.fetchStream` method. This method is useful when you want to receive real-time updates from the server. + +For example, consider you have the following custom API route at `src/api/admin/stream/route.ts`: + +```ts title="src/api/admin/stream/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) + + const interval = setInterval(() => { + res.write("data: Streaming data...\n\n") + }, 3000) + + req.on("close", () => { + clearInterval(interval) + res.end() + }) + + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} +``` + +Then, you can use the `client.fetchStream` method in a UI route to receive the streaming data: + +```tsx title="src/admin/route/stream/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading, Button, Text } from "@medusajs/ui" +import { useState } from "react" +import { sdk } from "../../lib/sdk" + +const StreamTestPage = () => { + const [messages, setMessages] = useState([]) + const [isStreaming, setIsStreaming] = useState(false) + const [abortStream, setAbortStream] = useState<(() => void) | null>(null) + + const startStream = async () => { + setIsStreaming(true) + setMessages([]) + + const { stream, abort } = await sdk.client.fetchStream("/admin/stream") + + if (!stream) { + console.error("Failed to start stream") + setIsStreaming(false) + return + } + + // Store the abort function for the abort button + setAbortStream(() => abort) + + try { + for await (const chunk of stream) { + // Since the server sends plain text, convert to string + const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk)) + setMessages((prev) => [...prev, message.trim()]) + } + } catch (error) { + // Don't log abort errors as they're expected when user clicks abort + if (error instanceof Error && error.name !== "AbortError") { + console.error("Stream error:", error) + } + } finally { + setIsStreaming(false) + setAbortStream(null) + } + } + + const handleAbort = () => { + if (abortStream) { + abortStream() + setIsStreaming(false) + setAbortStream(null) + } + } + + return ( + + + fetchStream Example + + +
+
+ + + +
+ +
+ {messages.length === 0 ? ( + No messages yet... + ) : ( + messages.map((msg, index) => ( +
+ {msg} +
+ )) + )} +
+
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Stream Test", +}) + +export default StreamTestPage +``` + +`fetchStream` accepts the same parameters as `fetch`, but it returns an object having two properties: + +- `stream`: An [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that you can use to iterate over the streaming data. +- `abort`: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server. + +In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI. + +*** + ## Handle Errors If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties: diff --git a/www/apps/resources/app/js-sdk/page.mdx b/www/apps/resources/app/js-sdk/page.mdx index 09e5f1ce71..d8e674af94 100644 --- a/www/apps/resources/app/js-sdk/page.mdx +++ b/www/apps/resources/app/js-sdk/page.mdx @@ -398,6 +398,151 @@ The method returns a Promise that, when resolved, has the data returned by the r --- +## Stream Server-Sent Events + +The JS SDK supports streaming server-sent events (SSE) using the `client.fetchStream` method. This method is useful when you want to receive real-time updates from the server. + +For example, consider you have the following custom API route at `src/api/admin/stream/route.ts`: + +```ts title="src/api/admin/stream/route.ts" +import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http" + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }) + + const interval = setInterval(() => { + res.write("data: Streaming data...\n\n") + }, 3000) + + req.on("close", () => { + clearInterval(interval) + res.end() + }) + + req.on("end", () => { + clearInterval(interval) + res.end() + }) +} +``` + +Then, you can use the `client.fetchStream` method in a UI route to receive the streaming data: + +```tsx title="src/admin/route/stream/page.tsx" +import { defineRouteConfig } from "@medusajs/admin-sdk" +import { Container, Heading, Button, Text } from "@medusajs/ui" +import { useState } from "react" +import { sdk } from "../../lib/sdk" + +const StreamTestPage = () => { + const [messages, setMessages] = useState([]) + const [isStreaming, setIsStreaming] = useState(false) + const [abortStream, setAbortStream] = useState<(() => void) | null>(null) + + const startStream = async () => { + setIsStreaming(true) + setMessages([]) + + const { stream, abort } = await sdk.client.fetchStream("/admin/stream") + + if (!stream) { + console.error("Failed to start stream") + setIsStreaming(false) + return + } + + // Store the abort function for the abort button + setAbortStream(() => abort) + + try { + for await (const chunk of stream) { + // Since the server sends plain text, convert to string + const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk)) + setMessages((prev) => [...prev, message.trim()]) + } + } catch (error) { + // Don't log abort errors as they're expected when user clicks abort + if (error instanceof Error && error.name !== "AbortError") { + console.error("Stream error:", error) + } + } finally { + setIsStreaming(false) + setAbortStream(null) + } + } + + const handleAbort = () => { + if (abortStream) { + abortStream() + setIsStreaming(false) + setAbortStream(null) + } + } + + return ( + + + fetchStream Example + + +
+
+ + + +
+ +
+ {messages.length === 0 ? ( + No messages yet... + ) : ( + messages.map((msg, index) => ( +
+ {msg} +
+ )) + )} +
+
+
+ ) +} + +export const config = defineRouteConfig({ + label: "Stream Test", +}) + +export default StreamTestPage +``` + +`fetchStream` accepts the same parameters as `fetch`, but it returns an object having two properties: + +- `stream`: An [AsyncGenerator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator) that you can use to iterate over the streaming data. +- `abort`: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server. + +In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI. + +--- + ## Handle Errors If an error occurs in a request, the JS SDK throws a `FetchError` object. This object has the following properties: diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index e0e1e4d391..739f38b651 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -2168,7 +2168,7 @@ export const generatedEditDates = { "app/commerce-modules/store/links-to-other-modules/page.mdx": "2025-04-17T16:03:16.419Z", "app/examples/page.mdx": "2025-07-16T09:53:26.163Z", "app/medusa-cli/commands/build/page.mdx": "2024-11-11T11:00:49.665Z", - "app/js-sdk/page.mdx": "2025-05-26T15:08:16.590Z", + "app/js-sdk/page.mdx": "2025-08-01T14:17:07.509Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.apiKey/page.mdx": "2025-05-20T07:51:40.924Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.campaign/page.mdx": "2025-05-20T07:51:40.925Z", "references/js_sdk/admin/Admin/properties/js_sdk.admin.Admin.claim/page.mdx": "2025-06-25T10:11:46.945Z",