Backend Requests

Between attributes and actions, Datastar provides you with everything you need to build hypermedia-driven applications. Using this approach, the backend drives state to the frontend and acts as the single source of truth, determining what actions the user can take next.

Sending Signals #

By default, all signals (except for local signals whose keys begin with an underscore) are sent in an object with every backend request. When using a GET request, the signals are sent as a datastar query parameter, otherwise they are sent as a JSON body.

By sending all signals in every request, the backend has full access to the frontend state. This is by design. It is not recommended to send partial signals, but if you must, you can use the filterSignals option to filter the signals sent to the backend.

Nesting Signals #

Signals can be nested, making it easier to target signals in a more granular way on the backend.

Using dot-notation:

1<div data-signals-foo.bar="1"></div>

Using object syntax:

1<div data-signals="{foo: {bar: 1}}"></div>

Using two-way binding:

1<input data-bind-foo.bar />

A practical use-case of nested signals is when you have repetition of state on a page. The following example tracks the open/closed state of a menu on both desktop and mobile devices, and the toggleAll() action to toggle the state of all menus at once.

1<div data-signals="{menu: {isOpen: {desktop: false, mobile: false}}}">
2    <button data-on-click="@toggleAll({include: /^menu\.isOpen\./})">
3        Open/close menu
4    </button>
5</div>

Reading Signals #

To read signals from the backend, JSON decode the datastar query param for GET requests, and the request body for all other methods.

All SDKs provide a helper function to read signals. Here’s how you would use the Go SDK to read the nested signal foo.bar in a request.

 1import ("github.com/starfederation/datastar/sdk/go/datastar")
 2
 3type Signals struct {
 4    Foo struct {
 5        Bar string `json:"bar"`
 6    } `json:"foo"`
 7}
 8
 9signals := &Signals{}
10if err := datastar.ReadSignals(request, signals); err != nil {
11    http.Error(w, err.Error(), http.StatusBadRequest)
12    return
13}

SSE Events #

Datastar can stream zero or more Server-Sent Events (SSE) from the web server to the browser. There’s no special backend plumbing required to use SSE, just some special syntax. Fortunately, SSE is straightforward and provides us with some advantages, in addition to allowing us to send multiple events in a single response (in contrast to sending text/html or application/json responses).

First, set up your backend in the language of your choice. Familiarize yourself with sending SSE events, or use one of the backend SDKs to get up and running even faster. We’re going to use the SDKs in the examples below, which set the appropriate headers and format the events for us.

The following code would exist in a controller action endpoint in your backend.

 1;; Import the SDK's api and your adapter
 2(require
 3  '[starfederation.datastar.clojure.api :as d*]
 4  '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]])
 5
 6
 7;; in a ring handler
 8(defn handler [request]
 9  ;; Create a SSE response
10  (->sse-response request
11   {on-open
12    (fn [sse]
13      ;; Merge html fragments into the DOM
14      (d*/patch-elements! sse
15        "<div id=\"question\">What do you put in a toaster?</div>")
16
17      ;; Merge signals into the signals
18      (d*/patch-signals! sse "{response: '', answer: 'bread'}"))}))
 1using StarFederation.Datastar.DependencyInjection;
 2
 3// Adds Datastar as a service
 4builder.Services.AddDatastar();
 5
 6app.MapGet("/", async (IDatastarService datastarService) =>
 7{
 8    // Patches elements into the DOM.
 9    await datastarService.PatchElementsAsync(@"<div id=""question"">What do you put in a toaster?</div>");
10
11    // Merges signals into the signals.
12    await sse.PatchSignalsAsync("{response: '', answer: 'bread'}");
13});
 1import ("github.com/starfederation/datastar/sdk/go/datastar")
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4sse := datastar.NewSSE(w,r)
 5
 6// Patches elements into the DOM.
 7sse.PatchElements(
 8    `<div id="question">What do you put in a toaster?</div>`
 9)
10
11// Merges signals into the signals.
12sse.PatchSignals([]byte(`{response: '', answer: 'bread'}`))
 1import starfederation.datastar.utils.ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4AbstractResponseAdapter responseAdapter = new HttpServletResponseAdapter(response);
 5ServerSentEventGenerator generator = new ServerSentEventGenerator(responseAdapter);
 6
 7// Patches elements into the DOM.
 8generator.send(PatchElements.builder()
 9    .data("<div id=\"question\">What do you put in a toaster?</div>")
10    .build()
11);
12
13generator.send(PatchSignals.builder()
14    .data("{\"response\": \"\", \"answer\": \"\"}")
15    .build()
16);
 1use starfederation\datastar\ServerSentEventGenerator;
 2
 3// Creates a new `ServerSentEventGenerator` instance.
 4$sse = new ServerSentEventGenerator();
 5
 6// Patches elements into the DOM.
 7$sse->patchElements(
 8    '<div id="question">What do you put in a toaster?</div>'
 9);
10
11// Merges signals into the signals.
12$sse->PatchSignals(['response' => '', 'answer' => 'bread']);
1from datastar_py import ServerSentEventGenerator as SSE
2from datastar_py.litestar import DatastarResponse
3
4async def endpoint():
5    return DatastarResponse([
6        SSE.patch_elements('<div id="question">What do you put in a toaster?</div>'),
7        SSE.patch_signals({"response": "", "answer": "bread"})
8    ])
 1require 'datastar'
 2
 3# Create a Datastar::Dispatcher instance
 4
 5datastar = Datastar.new(request:, response:)
 6
 7# In a Rack handler, you can instantiate from the Rack env
 8# datastar = Datastar.from_rack_env(env)
 9
10# Start a streaming response
11datastar.stream do |sse|
12  # Patches elements into the DOM
13  sse.patch_elements %(<div id="question">What do you put in a toaster?</div>)
14
15  # Patches signals
16  sse.patch_signals(response: '', answer: 'bread')
17end
 1use datastar::prelude::*;
 2use async_stream::stream;
 3
 4Sse(stream! {
 5    // Patches elements into the DOM.
 6    yield PatchElements::new("<div id='question'>What do you put in a toaster?</div>").into();
 7
 8    // Merges signals into the signals.
 9    yield PatchSignals::new("{response: '', answer: 'bread'}").into();
10})
1// Creates a new `ServerSentEventGenerator` instance (this also sends required headers)
2ServerSentEventGenerator.stream(req, res, (stream) => {
3      // Patches elements into the DOM.
4     stream.patchElements(`<div id="question">What do you put in a toaster?</div>`);
5
6     // Merges signals into the signals.
7     stream.patchSignals({'response':  '', 'answer': 'bread'});
8});

The PatchElements() function updates the provided HTML element into the DOM, replacing the element with id="question". An element with the ID question must already exist in the DOM.

The PatchSignals() function updates the response and answer signals into the frontend signals.

With our backend in place, we can now use the data-on-click attribute to trigger the @get() action, which sends a GET request to the /actions/quiz endpoint on the server when a button is clicked.

 1<div
 2    data-signals="{response: '', answer: ''}"
 3    data-computed-correct="$response.toLowerCase() == $answer"
 4>
 5    <div id="question"></div>
 6    <button data-on-click="@get('/actions/quiz')">Fetch a question</button>
 7    <button
 8        data-show="$answer != ''"
 9        data-on-click="$response = prompt('Answer:') ?? ''"
10    >
11        BUZZ
12    </button>
13    <div data-show="$response != ''">
14        You answered “<span data-text="$response"></span>”.
15        <span data-show="$correct">That is correct ✅</span>
16        <span data-show="!$correct">
17        The correct answer is “<span data-text="$answer"></span>” 🤷
18        </span>
19    </div>
20</div>

Now when the Fetch a question button is clicked, the server will respond with an event to modify the question element in the DOM and an event to modify the response and answer signals. We’re driving state from the backend!

Demo

...

You answered “”. That is correct ✅ The correct answer is “” 🤷

data-indicator #

The data-indicator attribute sets the value of a signal to true while the request is in flight, otherwise false. We can use this signal to show a loading indicator, which may be desirable for slower responses.

1<div id="question"></div>
2<button
3    data-on-click="@get('/actions/quiz')"
4    data-indicator-fetching
5>
6    Fetch a question
7</button>
8<div data-class-loading="$fetching" class="indicator"></div>
Demo

...

Indicator

Backend Actions #

We’re not limited to sending just GET requests. Datastar provides backend actions for each of the methods available: @get(), @post(), @put(), @patch() and @delete().

Here’s how we can send an answer to the server for processing, using a POST request.

1<button data-on-click="@post('/actions/quiz')">
2    Submit answer
3</button>

One of the benefits of using SSE is that we can send multiple events (patch elements and patch signals) in a single response.

1(d*/patch-elements! sse "<div id=\"question\">...</div>")
2(d*/patch-elements! sse "<div id=\"instructions\">...</div>")
3(d*/patch-signals! sse "{answer: '...', prize: '...'}")
1datastarService.PatchElementsAsync(@"<div id=""question"">...</div>");
2datastarService.PatchElementsAsync(@"<div id=""instructions"">...</div>");
3sse.PatchSignalsAsync("{answer: '...', prize: '...'}");
1sse.PatchElements(`<div id="question">...</div>`)
2sse.PatchElements(`<div id="instructions">...</div>`)
3sse.PatchSignals([]byte(`{answer: '...', prize: '...'}`))
 1generator.send(PatchElements.builder()
 2    .data("<div id=\"question\">...</div>")
 3    .build()
 4);
 5generator.send(PatchElements.builder()
 6    .data("<div id=\"instructions\">...</div>")
 7    .build()
 8);
 9generator.send(PatchSignals.builder()
10    .data("{\"answer\": \"...\", \"prize\": \"...\"}")
11    .build()
12);
1$sse->patchElements('<div id="question">...</div>');
2$sse->patchElements('<div id="instructions">...</div>');
3$sse->PatchSignals(['answer' => '...', 'prize' => '...']);
1return DatastarResponse([
2    SSE.patch_elements('<div id="question">...</div>'),
3    SSE.patch_elements('<div id="instructions">...</div>'),
4    SSE.patch_signals({"answer": "...", "prize": "..."})
5])
1datastar.stream do |sse|
2  sse.patch_elements('<div id="question">...</div>')
3  sse.patch_elements('<div id="instructions">...</div>')
4  sse.patch_signals(answer: '...', prize: '...')
5end
1yield PatchElements::new("<div id='question'>...</div>").into()
2yield PatchElements::new("<div id='instructions'>...</div>").into()
3yield PatchSignals::new("{answer: '...', prize: '...'}").into()
1stream.patchElements('<div id="question">...</div>');
2stream.patchElements('<div id="instructions">...</div>');
3stream.patchSignals({'answer': '...', 'prize': '...'});
In addition to your browser’s dev tools, the Datastar Inspector can be used to monitor and inspect SSE events received by Datastar.

Read more about SSE events in the reference.

Congratulations #

You’ve actually read the entire guide! You should now know how to use Datastar to build reactive applications that communicate with the backend using backend requests and SSE events.

Feel free to dive into the reference and explore the examples next, to learn more about what you can do with Datastar.