I was building an e-commerce dashboard recently, and I hit a wall. The page looked great on my fast connection, but on a slower mobile network, it just sat there. Blank. It was waiting for a massive JavaScript bundle to download before anything could happen. The user saw a page, but couldn’t click a button. That moment of frustration is why I’m writing this. There has to be a better way to build fast, interactive web apps from the very first pixel.
That search led me to a powerful combination: Qwik City and Drizzle ORM. This isn’t just another framework tutorial. It’s about a different way of thinking. Instead of sending all the code to the client and then “waking it up” (a process called hydration), Qwik sends a page that’s already awake. It’s ready to go. Drizzle ORM gives us a clean, type-safe way to talk to our database from this new architecture. Let’s build something real together.
We’ll create a product catalog. Think of a simple storefront. We need to list products, show details, and maybe have an admin section. The goal is instant interaction, no matter how big our app gets.
First, let’s set up our project. Open your terminal and run:
npm create qwik@latest qwik-drizzle-shop
cd qwik-drizzle-shop
npm install drizzle-orm postgres
npm install -D drizzle-kit @types/pg
This gives us a Qwik City project and adds Drizzle for database work. Now, we need to define what our data looks like. We’ll create a schema.ts file. This is our single source of truth for the database structure.
// src/db/schema.ts
import { pgTable, serial, text, numeric, integer, boolean } from 'drizzle-orm/pg-core';
export const products = pgTable('products', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
slug: text('slug').notNull().unique(),
price: numeric('price', { precision: 10, scale: 2 }).notNull(),
stock: integer('stock').notNull().default(0),
featured: boolean('featured').default(false),
});
See how that reads? It’s very close to writing SQL, but it’s TypeScript. We get autocomplete and type checking. Drizzle doesn’t hide SQL; it makes it safer. Next, we need a connection. We’ll create a simple utility to get our database client.
// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
export function getDb() {
const client = postgres(process.env.DATABASE_URL!);
return drizzle(client, { schema });
}
Now for the interesting part: Qwik City. Its routing is file-based. A folder like src/routes/products/ becomes a route. Inside, an index.tsx file is the page. But the magic is in the loader$ function. This runs only on the server. It fetches data and sends it along with the HTML. The browser never waits for JavaScript to fetch the initial product list.
Let’s make our homepage. We’ll create src/routes/index.tsx. The component is just for display. The data fetching happens separately.
// src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { getDb } from '~/db';
import { products } from '~/db/schema';
import { desc } from 'drizzle-orm';
// This runs on the SERVER
export const useFeaturedProducts = routeLoader$(async () => {
const db = getDb();
const featuredItems = await db
.select()
.from(products)
.where(eq(products.featured, true))
.orderBy(desc(products.createdAt))
.limit(4);
return featuredItems;
});
// This is the component that renders in the browser
export default component$(() => {
const productSignal = useFeaturedProducts();
const featuredProducts = productSignal.value;
return (
<div>
<h1>Featured Products</h1>
<div class="product-grid">
{featuredProducts.map((product) => (
<div key={product.id} class="product-card">
<h2>{product.name}</h2>
<p>${product.price}</p>
{/* The button is interactive IMMEDIATELY */}
<button onClick$={() => alert(`Added ${product.name}`)}>
Add to Cart
</button>
</div>
))}
</div>
</div>
);
});
Did you notice the onClick$ handler? Here’s where Qwik’s “resumability” clicks. That click handler isn’t active because JavaScript for the whole component downloaded. It’s because Qwik serialized the listener’s purpose into the HTML. Only when you click does the browser fetch the tiny piece of code for that specific action. The page is interactive before the main app JavaScript even finishes loading.
What about a dynamic page, like /products/wireless-headphones? Qwik City handles that elegantly. Create a file at src/routes/products/[slug]/index.tsx. The [slug] in brackets is a route parameter.
// src/routes/products/[slug]/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$, routeAction$ } from '@builder.io/qwik-city';
import { getDb } from '~/db';
import { products } from '~/db/schema';
import { eq } from 'drizzle-orm';
export const useProductData = routeLoader$(async ({ params }) => {
const db = getDb();
const [product] = await db
.select()
.from(products)
.where(eq(products.slug, params.slug));
if (!product) {
// Qwik City will automatically handle 404s
throw new Error('Product not found');
}
return product;
});
// Even form actions can be server-first
export const useAddToCart = routeAction$(async (data, requestEvent) => {
// This runs on the server when the form is submitted
const productId = data.productId;
// ... logic to add to a cart session
return { success: true, message: 'Item added' };
});
export default component$(() => {
const productSignal = useProductData();
const product = productSignal.value;
const cartAction = useAddToCart();
return (
<div>
<h1>{product.name}</h1>
<p>Price: ${product.price}</p>
<p>In Stock: {product.stock}</p>
<form action={cartAction}>
<input type="hidden" name="productId" value={product.id} />
<button type="submit">Add to Cart</button>
</form>
{cartAction.value?.success && (
<p>✅ {cartAction.value.message}</p>
)}
</div>
);
});
The form uses a Qwik City routeAction$. When submitted, it goes straight to the server, processes the request, and returns a result. This works even if JavaScript hasn’t loaded yet—a concept called progressive enhancement. The experience is robust.
So, what does this all add up to? You get a website that feels like a static page in its speed but behaves like a full single-page app. The Time to Interactive metric, which measures when a user can actually do something, becomes nearly instant. Your database queries are clean and type-safe with Drizzle. Your application logic is split into tiny, on-demand pieces with Qwik.
The best part? This isn’t theoretical. You can start applying these concepts to parts of your existing projects. The combination of server-driven data with resumable client interactivity solves real performance problems that users face every day.
Have you ever measured how long it takes for your app to become truly interactive? The results might surprise you. Try building a single route this way and see the difference. I’d love to hear what you build. If this approach to performance makes sense to you, share it with a teammate who’s also tired of the “hydration tax.” Let me know in the comments what you think the biggest hurdle is in adopting this pattern.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva