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 not terminate immediately. It depends on how client disconnections are handled by the 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* () {
// Creation of an array with 10 undefined values
const iterationRange = Array(10).fill().entries();
for (const [index] of iterationRange) {
// Yield returns the index during each iteration
yield index;
// Waiting for 1 second before the next iteration
// This simulates a delay in the execution
await new Promise((resolve) => setTimeout(resolve, 1000));
}
});
export default component$(() => {
const message = useSignal('');
return (
<div>
<button
onClick$={async () => {
// call the async stream function and wait for the response
const response = await stream();
// use a for-await-of loop to asynchronously iterate over the response
for await (const index of response) {
// add each index from the response to the message value
message.value += ` ${index}`;
}
}}
>
start
</button>
<div>{message.value}</div>
</div>
);
});
This API is actually used to implement QwikGPT streaming responses in our docs site.
server$()
work?
How does 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.
Important Gotcha When defining and calling
server$()
inside anonClick$
, be aware that this can lead to potential errors. To avoid them, make sure the handlers have$
wrapped around them.
Don't do this
onClick$={() => server$(() => // some server code!)}
Do this
onClick$={$(() => server$(() => // some server code!))}
server$
Middleware and 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.