Build generative UI with OpenUI — any LLM provider, any backend language. Scaffold, integrate, validate.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
OpenUI generative UI with a Java Spring Boot (WebFlux) backend. Streams the OpenAI API directly via WebClient as SSE.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
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 |
아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
Build generative UI with OpenUI — any LLM provider, any backend language. Scaffold, integrate, validate.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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.
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.