server$()

server$() allows you to create a function that always execute on the server, making it a great place to access the DB or perform server-only actions.

server$ is a form of RPC (Remote Procedure Call) mechanism between the client and server, just like a traditional HTTP endpoint but strongly typed thanks to Typescript, and easier to maintain.

Your new function will have the following signature: ([AbortSignal, ] ...): Promise<T>

AbortSignal is optional, and allows you to cancel a long running request by terminating the connection. Please note that depending on your server runtime, the function on the server may or may not terminate immediately as it depends on how client disconnections are handled by said runtime.

import { component$, useSignal } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
// By wrapping a function with `server$()` we mark it to always
// execute on the server. This is a form of RPC mechanism.
const serverGreeter = server$((firstName: string, lastName: string) => {
  const greeting = `Hello ${firstName} ${lastName}`;
  console.log('Prints in the server', greeting);
  return greeting;
});
 
export default component$(() => {
  const firstName = useSignal('');
  const lastName = useSignal('');
 
  return (
    <section>
      <label>First name: <input bind:value={firstName} /></label>
      <label>Last name: <input bind:value={lastName} /></label>
 
      <button
        onClick$={async () => {
          const greeting = await serverGreeter(firstName.value, lastName.value);
          alert(greeting);
        }}
      >
        greet
      </button>
    </section>
  );
});

server$ can also read the HTTP cookies, headers, or environment variables, using this. In this case you will need to use a function instead of an arrow function.

// Notice that the wrapped function is declared as an `async function`
const addUser = server$(async function(id: number, fullName: string, address: Address) {
  // The `this` is the `RequestEvent` object, which contains
  // the HTTP headers, cookies, and environment variables.
  const db = createClient(this.env.get('DB_KEY'));
  if (this.cookie.get('user-session')) {
    await db.from('users').insert({
      id,
      fullName,
      address
    });
    return {
      success: true,
    }
  }
  return {
    success: false,
  }
})

Server$ can accept any number of arguments and return any value that can be serialized by Qwik, that includes primitives, objects, arrays, bigint, JSX nodes and even Promises, just to name a few.

Streaming responses

server$ can return a stream of data by using an async generator. This is useful for streaming data from the server to the client.

Terminating the generator on the client side (e.g. by calling return() on the generator, or by breaking out from your async for-loop) will terminate the connection. As with AbortSignal - how the generator will terminate on the server side depends on the server runtime and how client disconnects are handled.

import { component$, useSignal } from '@builder.io/qwik';
import { server$ } from '@builder.io/qwik-city';
 
const stream = server$(async function* () {
  for (let i = 0; i < 10; i++) {
    yield i;
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
});
 
export default component$(() => {
  const message = useSignal('');
  return (
    <div>
      <button
        onClick$={async () => {
          const response = await stream();
          for await (const i of response) {
            message.value += ` ${i}`;
          }
        }}
      >
        start
      </button>
      <div>{message.value}</div>
    </div>
  );
});

This API is actually used to implement QwikGPT streaming responses in our docs site.

How does server$() work?

A server$() wraps a function and returns an async proxy to the function. On the server, the proxy function directly calls the wrapped function, and a HTTP endpoint is automatically created by the server$() function.

On the client, the proxy function invokes the wrapped function via an HTTP request, using fetch().

Note: The server$() function must ensure that the server and client have the same version of the code executing. If there is a version skew the behavior is undefined and may result in an error. If version skew is a common problem then a more formal RPC mechanism should be used such as a tRPC or other library.

Middleware and server$

When using server$, it's important to understand how middleware functions are executed. Middleware functions defined in layout files do not run for server$ requests. This can lead to confusion, especially when developers expect certain middleware to be executed for both page requests and server$ requests.

To ensure that a middleware function runs for both types of requests, it should be defined in the plugin.ts file. This ensures that the middleware is executed consistently for all incoming requests, regardless of whether they are normal page requests or server$ requests.

By defining middleware in the plugin.ts file, developers can maintain a centralized location for shared middleware logic, ensuring consistency and reducing potential errors or oversights.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • mhevery
  • manucorporat
  • AnthonyPAlicea
  • the-r3aper7
  • igorbabko
  • RaeesBhatti
  • mrhoodz
  • DanielAdolfsson
  • mjschwanitz
  • wtlin1228
  • adamdbradley