Qwik takes a radical approach to web performance: zero JavaScript hydration. Its resumability model and server-side APIs make it one of the fastest frameworks available.
Resumability — No Hydration
Qwik serializes component state to HTML. When the user interacts, only the needed code loads:
import { component$, useSignal } from "@builder.io/qwik"
export const Counter = component$(() => {
const count = useSignal(0)
return (
<div>
<p>Count: {count.value}</p>
<button onClick$={() => count.value++}>
Increment
</button>
</div>
)
})
// onClick$ handler downloads ONLY when clicked — not at page load!
Server Loaders — Data Fetching
import { component$ } from "@builder.io/qwik"
import { routeLoader$ } from "@builder.io/qwik-city"
export const useProducts = routeLoader$(async ({ query, env }) => {
const page = Number(query.get("page") || 1)
const apiKey = env.get("API_KEY")
const res = await fetch(`https://api.store.com/products?page=${page}`, {
headers: { Authorization: `Bearer ${apiKey}` }
})
return res.json()
})
export default component$(() => {
const products = useProducts()
return (
<div>
{products.value.map(p => (
<div key={p.id}>
<h3>{p.name}</h3>
<p>${p.price}</p>
</div>
))}
</div>
)
})
Server Actions — Mutations
import { component$ } from "@builder.io/qwik"
import { routeAction$, zod$, z, Form } from "@builder.io/qwik-city"
export const useCreatePost = routeAction$(
async (data, { fail }) => {
const post = await db.post.create({ data })
if (!post) return fail(500, { message: "Failed to create" })
return { success: true, id: post.id }
},
zod$({
title: z.string().min(3),
content: z.string().min(10)
})
)
export default component$(() => {
const action = useCreatePost()
return (
<Form action={action}>
<input name="title" />
{action.value?.fieldErrors?.title && (
<p>{action.value.fieldErrors.title}</p>
)}
<textarea name="content" />
<button type="submit">
{action.isRunning ? "Saving..." : "Create"}
</button>
{action.value?.success && <p>Post created!</p>}
</Form>
)
})
API Endpoints
// src/routes/api/users/index.tsx
import type { RequestHandler } from "@builder.io/qwik-city"
export const onGet: RequestHandler = async ({ json, query }) => {
const limit = Number(query.get("limit") || 10)
const users = await db.user.findMany({ take: limit })
json(200, users)
}
export const onPost: RequestHandler = async ({ json, parseBody }) => {
const body = await parseBody()
const user = await db.user.create({ data: body })
json(201, user)
}
export const onDelete: RequestHandler = async ({ json, query }) => {
const id = query.get("id")
await db.user.delete({ where: { id } })
json(200, { deleted: true })
}
Middleware
// src/routes/layout.tsx
import type { RequestHandler } from "@builder.io/qwik-city"
export const onRequest: RequestHandler = async ({ next, cookie, sharedMap }) => {
const token = cookie.get("session")?.value
if (token) {
const user = await verifyToken(token)
sharedMap.set("user", user)
}
await next()
}
useTask$ — Server/Client Effects
import { component$, useTask$, useSignal } from "@builder.io/qwik"
import { isServer } from "@builder.io/qwik/build"
export default component$(() => {
const data = useSignal(null)
useTask$(async ({ track }) => {
if (isServer) {
// Runs on server during SSR
data.value = await fetchFromDB()
}
})
return <pre>{JSON.stringify(data.value, null, 2)}</pre>
})
Key Takeaways
- Resumability — zero hydration, instant interactivity
- routeLoader$ for server-side data fetching
- routeAction$ for type-safe mutations with Zod
- API endpoints with onGet/onPost/onDelete handlers
- Middleware for auth and request processing
- Lazy loading at the event handler level
Explore Qwik docs for the complete API.
Building web scrapers or data pipelines? Check out my Apify actors for ready-made solutions, or email spinov001@gmail.com for custom development.
Top comments (0)