ワンクリックで
openui-forge-java
OpenUI generative UI with a Java Spring Boot (WebFlux) backend. Streams the OpenAI API directly via WebClient as SSE.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
OpenUI generative UI with a Java Spring Boot (WebFlux) backend. Streams the OpenAI API directly via WebClient as SSE.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
OpenUI generative UI with Anthropic Claude SDK backend. Stream conversion to OpenAI NDJSON format.
OpenUI generative UI with C# ASP.NET Core Minimal API backend. Direct OpenAI API SSE streaming via HttpClient on .NET 10.
OpenUI generative UI with Elixir Phoenix backend. Chunked SSE streaming via Plug.Conn and Req.
OpenUI generative UI with Go (net/http) backend. Direct OpenAI API streaming via HTTP.
OpenUI generative UI with LangChain/LangGraph backend. Supports ChatOpenAI and ChatAnthropic.
OpenUI generative UI with OpenAI SDK backend. Streaming chat completions with gpt-5.5 (or any current OpenAI-compatible model).
| name | openui-forge-java |
| description | OpenUI generative UI with a Java Spring Boot (WebFlux) backend. Streams the OpenAI API directly via WebClient as SSE. |
| version | 1.2.0 |
| author | OthmanAdi |
Build generative UI apps with a React frontend + Java Spring Boot (WebFlux) backend. The backend forwards OpenAI's SSE stream to the browser via a reactive WebClient and a Flux<String> controller, pairing with openAIAdapter() on the frontend.
OPENAI_API_KEY environment variable setnpm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
npx @openuidev/cli generate ./src/lib/library.ts --out backend/src/main/resources/system-prompt.txt
mvn spring-boot:run on :8080, frontend on :3000backend/pom.xml<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>openui-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- WebFlux pulls in the reactive web stack, WebClient, and Reactor Netty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
backend/src/main/resources/application.properties# Server port (override with SERVER_PORT env var). Frontend expects 8080.
server.port=${SERVER_PORT:8080}
backend/src/main/java/com/example/openui/Application.javapackage com.example.openui;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
// CORS — lock to the configured frontend origin. Never use "*" here: the
// endpoint is browser-callable and a wildcard lets any site burn your key.
@Bean
public WebFluxConfigurer corsConfigurer(
@Value("${FRONTEND_ORIGIN:http://localhost:3000}") String frontendOrigin) {
return new WebFluxConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(frontendOrigin)
.allowedMethods("POST", "OPTIONS")
.allowedHeaders("Content-Type")
.allowCredentials(true);
}
};
}
// Shared non-blocking WebClient (Reactor Netty). Honors OPENAI_BASE_URL so
// OpenAI-compatible providers (Azure, Groq, Together, Ollama, ...) work.
@Bean
public WebClient openAiWebClient(
WebClient.Builder builder,
@Value("${OPENAI_BASE_URL:https://api.openai.com/v1}") String baseUrl) {
return builder.baseUrl(baseUrl).build();
}
}
record ChatMessage(String role, String content) {}
record ChatRequest(List<ChatMessage> messages) {}
@RestController
class ChatController {
private final WebClient openAiWebClient;
private final String apiKey;
private final String model;
private final String systemPrompt;
ChatController(
WebClient openAiWebClient,
@Value("${OPENAI_API_KEY:}") String apiKey,
@Value("${OPENAI_MODEL:gpt-5.5}") String model,
@Value("classpath:system-prompt.txt") Resource systemPromptResource) {
this.openAiWebClient = openAiWebClient;
this.apiKey = apiKey;
this.model = model;
try {
// Loaded ONCE at startup from src/main/resources/system-prompt.txt.
this.systemPrompt = systemPromptResource.getContentAsString(StandardCharsets.UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(
"Failed to read system-prompt.txt from classpath. Generate it with: "
+ "npx @openuidev/cli generate ./src/lib/library.ts "
+ "--out src/main/resources/system-prompt.txt", e);
}
}
// produces=text/event-stream + Flux<String>: Spring's SSE writer wraps each
// emitted String as "data: <element>\n\n". We emit BARE JSON payloads (upstream
// "data:" prefix already stripped by the SSE reader) so the wire output is
// exactly "data: {chunk}\n\n" ... "data: [DONE]\n\n" — what openAIAdapter() expects.
@PostMapping(value = "/api/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> chat(@RequestBody ChatRequest request) {
if (apiKey == null || apiKey.isBlank()) {
return Flux.just("{\"error\":\"OPENAI_API_KEY not set\"}");
}
// Prepend the system prompt server-side; never trust a client system message.
List<ChatMessage> messages = new ArrayList<>();
messages.add(new ChatMessage("system", systemPrompt));
if (request.messages() != null) {
messages.addAll(request.messages());
}
Map<String, Object> upstreamBody = Map.of(
"model", model,
"stream", true,
"messages", messages);
// bodyToFlux(String.class) on a text/event-stream response makes Spring's
// ServerSentEventHttpMessageReader parse upstream "data:" events and emit
// each event's bare payload incrementally (buffered across TCP chunks).
return openAiWebClient.post()
.uri("/chat/completions")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.TEXT_EVENT_STREAM)
.bodyValue(upstreamBody)
.retrieve()
.bodyToFlux(String.class);
}
}
app/chat/page.tsx"use client";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import {
openAIAdapter,
openAIMessageFormat,
} from "@openuidev/react-headless";
export default function ChatPage() {
return (
<FullScreen
componentLibrary={openuiChatLibrary}
streamProtocol={openAIAdapter()}
messageFormat={openAIMessageFormat}
apiUrl="http://localhost:8080/api/chat"
/>
);
}
The Spring Boot backend forwards OpenAI's SSE stream incrementally:
WebClient.bodyToFlux(String.class)parses upstreamdata:events as they arrive, and theFlux<String>controller (produces=text/event-stream) re-frames each payload asdata: {chunk}\n\n, ending withdata: [DONE]\n\n. Reactor is fully non-blocking, so tokens flush to the client as they stream (no buffering of the whole response). Pair it withopenAIAdapter()on the frontend.openAIReadableStreamAdapter()is for NDJSON (nodata:prefix) and will silently produce no output here.Because the controller emits
Flux<String>withtext/event-stream, Spring'sServerSentEventHttpMessageWriteradds thedata:prefix for you. Do not prependdata:to the strings yourself or you will get a doubleddata: data: {...}frame that the adapter cannot parse.
npx @openuidev/cli generate ./src/lib/library.ts --out backend/src/main/resources/system-prompt.txt
system-prompt.txt exists in backend/src/main/resources/OPENAI_API_KEY is set in the environmentOPENAI_BASE_URL set if using an OpenAI-compatible provider (default https://api.openai.com/v1)FRONTEND_ORIGIN, default http://localhost:3000), not a wildcardproduces = MediaType.TEXT_EVENT_STREAM_VALUE and returns Flux<String>data: prefix — Spring adds it)apiUrl points to http://localhost:8080/api/chatstreamProtocol={openAIAdapter()} and openAIMessageFormatcomponentLibrary={openuiChatLibrary} prop passed to FullScreen@openuidev/react-ui/components.css)| Error | Cause | Fix |
|---|---|---|
| CORS blocked | Origin mismatch | Set FRONTEND_ORIGIN to the frontend URL; check allowedOrigins |
FileNotFoundException / UncheckedIOException on startup | system-prompt.txt missing from src/main/resources/ | Run the CLI generate command into that path, then rebuild |
| 401 from upstream | OPENAI_API_KEY unset or invalid | Export a valid key; verify OPENAI_BASE_URL matches the provider |
WebClientResponseException 4xx/5xx | Bad model or provider error | Check OPENAI_MODEL and OPENAI_BASE_URL; inspect the exception body |
Doubled data: data: frames | Manually prefixed strings with data: | Emit bare JSON; the SSE writer adds the prefix |
| Empty render / no output | Frontend used openAIReadableStreamAdapter() (NDJSON) | Use openAIAdapter() to match the SSE body |
| Response buffers then dumps at once | Returned Mono/collected the Flux | Return the Flux<String> directly so elements stream as they arrive |