Backend Development 8 min read

Simulating ChatGPT‑Style Typing with Spring WebFlux and SSE

This tutorial demonstrates how to use Spring WebFlux’s reactive streaming to create a ChatGPT‑like typing effect, covering backend setup, SSE integration, frontend Axios handling, and a comparison between Flux and traditional Server‑Sent Events.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Simulating ChatGPT‑Style Typing with Spring WebFlux and SSE

Environment: Spring Boot 3.0.9

1. Introduction

This article explores how to use Spring WebFlux to simulate the character‑by‑character typing effect seen in ChatGPT. By leveraging WebFlux’s non‑blocking, real‑time data transmission, developers can create smooth, live user experiences for modern web applications.

2. Environment Preparation

Server side

Include the WebFlux starter dependency instead of the traditional servlet‑based starter:

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-webflux&lt;/artifactId&gt;
&lt;/dependency&gt;</code>

Do not import both spring-boot-starter-web and spring-boot-starter-webflux together; the servlet starter will take precedence.

Frontend

The front end uses Axios for asynchronous HTTP requests.

3. Practical Example

Create a simple WebFlux endpoint that emits 20 messages, each delayed by one second:

<code>@GetMapping("")
public Flux&lt;String&gt; message() {
    return Flux
        .range(1, 20)
        .map(n -&gt; "message - " + n + "&lt;br/&gt;")
        .delayElements(Duration.ofMillis(1000));
}</code>

The result is a stream of messages displayed one after another.

Next, set up a static resource directory /sse with an index.html file and configure Spring to serve it:

<code>@Configuration
public class StaticResourceConfig implements WebFluxConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/sse/**")
                .addResourceLocations("classpath:/sse/");
    }
}</code>

Sample index.html (simplified):

<code>&lt;html&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;title&gt;WebFlux Typing Effect&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h2&gt;WebFlux Typing Effect&lt;/h2&gt;
  &lt;/body&gt;
&lt;/html&gt;</code>

Define a controller that receives plain‑text content, splits it into characters, and streams each character with a one‑second delay:

<code>@RestController
@RequestMapping("/sse")
public class SseController {
    @PostMapping(value = "", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux&lt;String&gt; message(@RequestBody String content) {
        return Flux
            .just(content)
            .flatMap(c -&gt; Flux.just(c.split("")))
            .map(n -&gt; "message - " + n + ", ")
            .delayElements(Duration.ofMillis(1000));
    }
}</code>

Front‑end HTML for input and result display:

<code>&lt;div&gt;
  &lt;h4&gt;Result&lt;/h4&gt;
  &lt;div class="result"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;table&gt;
    &lt;tr&gt;
      &lt;td&gt;
        &lt;textarea id="content"&gt;&lt;/textarea&gt;
      &lt;/td&gt;
      &lt;td&gt;
        &lt;button type="button" onclick="submitContent()"&gt;Submit&lt;/button&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/table&gt;
&lt;/div&gt;</code>

JavaScript using Axios to post the content and handle progress events:

<code>const instance = axios.create({
  baseURL: 'http://localhost:8080',
  timeout: 10000
});
function submitContent() {
  let content = document.querySelector('#content').value;
  instance.post('/sse', content, {
    headers: {'Content-Type': 'text/plain'},
    onDownloadProgress(progressEvent) {
      // Decode only the newly received bytes
      const text = progressEvent.event.currentTarget.responseText;
      const encoder = new TextEncoder();
      const decoder = new TextDecoder('UTF-8');
      const byteArray = encoder.encode(text)
        .subarray(progressEvent.loaded - progressEvent.bytes, progressEvent.loaded);
      console.log(decoder.decode(byteArray));
    }
  }).then(resp => console.log(resp)).catch(ex => console.error(`${ex}`));
}</code>

The above approach avoids the issue where each progress event contains the entire accumulated response; by extracting only the newly received bytes, the typing effect appears character by character.

Attempting to set responseType: 'stream' in the browser fails because the XMLHttpRequest spec does not support a "stream" response type; this mode is only available in Node.js environments.

4. Flux vs. SSE

WebFlux returns a Flux , which is a reactive stream that can push multiple data items over time, keeping the connection open for continuous delivery. SSE (Server‑Sent Events) is a simpler HTTP‑based protocol that also pushes data but offers fewer features and is limited to plain text events.

Both technologies enable server‑to‑client push, yet they differ in data handling, reconnection logic, and extensibility.

Finished!

JavaStreamingAxiosReactive StreamsServer-Sent EventsSpring WebFlux
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.