server$()
server$()
allows you to define functions that execute exclusively on the server, making it ideal for server-only operations and database access. It functions as an RPC (Remote Procedure Call) mechanism between the client and server. This is similar to a traditional HTTP endpoint, but strongly typed with TypeScript and easier to maintain.
server$
can accept any number of arguments and return any value that can be serialized by Qwik. This includes primitives, objects, arrays, bigint, JSX nodes, and even Promises, just to name a few.
AbortSignal
is optional, and allows you to cancel a long running request by terminating the connection.
Your new function will have the following signature:
([AbortSignal, ...yourOtherArgs]): Promise<T>
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.
export const serverGreeter = server$(
function (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>
);
});
RequestEvent
Accessing Request Information with When using server$
, you have access to the RequestEvent
object through this
. This object provides useful information about the HTTP request, including environment variables, cookies, URL, and headers. Here's how you can use it:
Environment Variables
You can access environment variables using this.env.get()
.
export const getEnvVariable = server$(
function () {
const dbKey = this.env.get('DB_KEY');
console.log('Database Key:', dbKey);
return dbKey;
}
);
Cookies
You can read cookies using this.cookie.get()
and this.cookie.set()
.
When using
handleCookies
(in our example below) if it's used within auseTask$
function that runs during the initial request, setting cookies wonโt work as expected. This is because, during server-side rendering (SSR), the response is streamed, and HTTP requires all headers to be set before sending the first response. However, if handleCookies is used in useVisibleTask$, this issue doesnโt occur. If you need to set cookies for the initial document request you can useplugin@<name>.ts
or Middleware.
export const handleCookies = server$(
function () {
const userSession = this.cookie.get('user-session')?.value;
if (!userSession) {
this.cookie.set('user-session', 'new-session-id', { path: '/', httpOnly: true });
}
return userSession;
}
);
URL
You can access the request URL and its components using this.url
.
export const getRequestUrl = server$(
function () {
const requestUrl = this.url;
console.log('Request URL:', requestUrl);
return requestUrl;
}
);
Headers
You can read headers using this.headers.get()
.
export const getHeaders = server$(
function () {
const userAgent = this.headers.get('User-Agent');
console.log('User-Agent:', userAgent);
return userAgent;
}
);
Using Multiple RequestEvent Information
Here's an example that combines environment variables, cookies, URL, and headers in a single function.
export const handleRequest = server$(
function () {
// Access environment variable
const dbKey = this.env.get('DB_KEY');
// Access cookies
const userSession = this.cookie.get('user-session')?.value;
if (!userSession) {
this.cookie.set('user-session', 'new-session-id', { path: '/', httpOnly: true });
}
// Access request URL
const requestUrl = this.url;
// Access headers
const userAgent = this.headers.get('User-Agent');
console.log('Environment Variable:', dbKey);
console.log('User Session:', userSession);
console.log('Request URL:', requestUrl);
console.log('User-Agent:', userAgent);
return {
dbKey,
userSession,
requestUrl,
userAgent
};
}
);
Streaming Responses
server$
can return a stream of data by using an async generator function, which 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-of loop) will terminate the connection. Similar to AbortSignal
, how the generator terminates 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';
export const streamFromServer = server$(
// Async Generator Function
async function* () {
// Creation of an array with 10 undefined values
const iterationRange = Array(10).fill().entries();
for (const [value] of iterationRange) {
// Yield returns the array value during each iteration
yield value;
// 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 streamFromServer();
// use a for-await-of loop to asynchronously iterate over the response
for await (const value of response) {
// add each value from the response to the message value
message.value += ` ${value}`;
}
// do anything else
}
}
>
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 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.