TechSetupGuides
Advancedastrocloudflarer2drizzleqwikserverlessedge-computingjamstack

Edge-First Serverless Stack: Astro 5 + Cloudflare R2 + Drizzle ORM + Qwik

Build a modern serverless application with Astro 5 Islands architecture, Cloudflare R2 storage, Drizzle ORM, Qwik resumability, and edge caching.

  1. Step 1

    Project Initialization with Astro 5

    Start by creating a new Astro project with TypeScript. Astro 5 introduces improved Islands architecture for partial hydration, allowing you to deliver mostly-static HTML with selective interactivity.

    npm create astro@latest edge-serverless-stack
    # Select options:
    # - TypeScript: Yes
    # - Install dependencies: Yes
    # - Initialize git: Yes
    # - ASTRO_DB: No (using Drizzle separately)
  2. Step 2

    Install Core Dependencies

    Install the core packages for the stack: Qwik for resumable components, Drizzle ORM for type-safe database operations, and Cloudflare Workers SDK.

    npm add @builder.io/qwik @builder.io/qwik-city
    npm add drizzle-orm drizzle-kit @electric-sql/pglite
    npm add wrangler @cloudflare/workers-types
    npm add -D typescript @types/node
  3. Step 3

    Configure Astro 5 with Qwik Integration

    Set up Astro to work with Qwik by installing the Qwik adapter. Configure the project to use Cloudflare Workers as the deployment target.

    // astro.config.mjs
    import defineConfig from 'astro/config';
    import qwik from '@astrojs/qwik';
    import cloudflare from '@astrojs/cloudflare';
    
    export default defineConfig({
      integrations: [
        qwik({
          // Qwik configuration
          devRequestHandler: true,
        }),
      ],
      adapter: cloudflare({
        routes: [
          { pattern: '/api/*', environment: 'api' },
          { pattern: '/assets/*', environment: 'assets' },
        ],
      }),
      build: {
        client: 'dist/client',
        server: 'dist/server',
      },
    });
  4. Step 4

    Set Up Drizzle ORM Schema and Migrations

    Define your database schema using Drizzle ORM. Drizzle provides type-safe SQL queries and works well with edge databases like Cloudflare D1 or external PostgreSQL instances.

    // src/db/schema.ts
    import { pgTable, varchar, timestamp, uuid } from 'drizzle-orm/pg-core';
    
    export const posts = pgTable('posts', {
      id: uuid('id').primaryKey().defaultRandom(),
      title: varchar('title', { length: 255 }).notNull(),
      slug: varchar('slug', { length: 255 }).notNull().unique(),
      content: varchar('content', { length: 10000 }).notNull(),
      publishedAt: timestamp('published_at').notNull().defaultNow(),
      authorId: uuid('author_id').notNull().references(() => users.id),
    });
    
    export const users = pgTable('users', {
      id: uuid('id').primaryKey().defaultRandom(),
      email: varchar('email', { length: 255 }).notNull().unique(),
      name: varchar('name', { length: 100 }).notNull(),
      createdAt: timestamp('created_at').notNull().defaultNow(),
    });
  5. Step 5

    Configure Database Connection for Edge

    Set up the Drizzle connection to work at the edge. Use connection pooling and async connections for optimal performance.

    // src/db/drizzle.ts
    import { drizzle } from 'drizzle-orm/peggy';
    import * as schema from './schema';
    
    export function getDb() {
      const db = drizzle({
        schema,
        logger: true,
      });
      return db;
    }
    
    // For Cloudflare D1 or external Postgres
    // src/db/d1.ts
    import { drizzle } from 'drizzle-orm/d1';
    import * as schema from './schema';
    
    export function getD1(db: D1Database) {
      return drizzle(db, { schema });
    }
  6. Step 6

    Configure Cloudflare R2 for Object Storage

    Set up Cloudflare R2 for storing user uploads, images, and static assets. R2 provides S3-compatible storage with no egress fees.

    // wrangler.toml
    name = "edge-serverless-stack"
    compatibility_date = "2024-01-01"
    
    [[r2_buckets]]
      binding = "STORAGE"
      bucket_name = "my-app-bucket"
      preview_bucket_name = "my-app-bucket-preview"
    
    [[d1_databases]]
      binding = "DB"
      database_name = "my-app-db"
      database_id = "your-database-id"
      local_binding = true
  7. Step 7

    Create R2 Service for File Operations

    Build a service layer for R2 operations including file uploads, downloads, and signed URL generation for secure temporary access.

    // src/services/r2.ts
    import { createClient } from '@upstash/ratelimit';
    
    export class R2Service {
      private storage: R2Bucket;
    
      constructor(storage: R2Bucket) {
        this.storage = storage;
      }
    
      async uploadFile(
        key: string,
        body: BodyInit,
        metadata?: Record<string, string>
      ): Promise<R2Object> {
        return await this.storage.put(key, body, {
          httpMetadata: new ResponseMetadata({
            contentType: this.detectContentType(key),
          }),
          customMetadata: metadata,
        });
      }
    
      async generateSignedUrl(key: string, expiresInSeconds: number = 3600) {
        const object = await this.storage.get(key);
        if (!object) throw new Error('File not found');
        return await object.getSignedUrl({
          expiresIn: expiresInSeconds,
          parameters: { 'response-content-disposition': `attachment; filename="${key}"` } as any,
        });
      }
    
      private detectContentType(key: string): string {
        const extMap: Record<string, string> = {
          '.jpg': 'image/jpeg',
          '.png': 'image/png',
          '.gif': 'image/gif',
          '.webp': 'image/webp',
          '.pdf': 'application/pdf',
        };
        const ext = key.slice(key.lastIndexOf('.')).toLowerCase();
        return extMap[ext] || 'application/octet-stream';
      }
    }
  8. Step 8

    Implement Astro Islands with Partial Hydration

    Create interactive islands using Astro's partial hydration. Islands allow you to keep most of your HTML static while only hydrating specific interactive components on the client.

    -- src/components/InteractivePost.astro
    <script>
      import { Component as QwikComponent } from '@builder.io/qwik';
      import PostCard from './PostCard.astro';
    </script>
    
    <!-- Non-interactive content renders as static HTML -->
    <article>
      <h1>{props.title}</h1>
      <p>{props.excerpt}</p>
      
      <!-- Island: Only this section gets hydrated -->
      <client:load>
        <PostCard title={props.title} content={props.content} />
      </client:load>
    </article>
    
    <!-- Alternative using Qwik's resumability -->
    <!-- src/components/QwikPost.astro -->
    <script>
      import { component$ } from '@builder.io/qwik';
      import { useStore } from '@builder.io/qwik';
    
      const QwikPost = component$(() => {
        const state = useStore({ loading: true });
        
        return (
          <article>
            <h1>{props.title}</h1>
            {state.loading ? <LoadingSpinner /> : <PostContent />}
          </article>
        );
      });
    </script>
  9. Step 9

    Build Edge Functions for Serverless Logic

    Create edge functions that run close to your users for low-latency API responses. Edge functions can handle authentication, data fetching, and real-time processing.

    // src/pages/api/posts/[slug]/[slug].ts
    import type { APIRoute } from 'astro';
    
    export async function GET(
      { params, request, context }: APIRoute
    ) {
      const { slug } = await params;
      
      // Cache control for edge caching
      const cacheControl = request.headers.get('x-cache-control');
      
      // Fetch from database or cache
      const post = await getPostBySlug(slug);
      
      if (!post) {
        return new Response('Not found', { status: 404 });
      }
    
      return new Response(JSON.stringify(post), {
        headers: {
          'Content-Type': 'application/json',
          'Cache-Control': cacheControl || 'public, max-age=300, stale-while-revalidate=60',
          'Vary': 'Accept-Encoding',
        },
      });
    }
  10. Step 10

    Implement Edge Caching Strategy

    Configure HTTP caching at the edge using Cache-Control headers and Cloudflare's caching mechanisms. Implement stale-while-revalidate for optimal performance.

    // src/middleware/cache.ts
    import type { APIContext } from 'astro';
    
    export async function cacheMiddleware(
      context: APIContext,
      next: () => Promise<Response>
    ): Promise<Response> {
      const response = await next();
      
      // Cache static assets for extended period
      if (context.url.pathname.startsWith('/assets/')) {
        response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
      }
      // Cache API responses with validation
      else if (context.url.pathname.startsWith('/api/')) {
        response.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
        response.headers.set('Stale', 'while-revalidate');
      }
      // Cache HTML pages with short TTL
      else {
        response.headers.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=3600');
      }
      
      return response;
    }
    
    // Wrangler configuration for Cloudflare cache
    // wrangler.toml
    [site]
      bucket = "static-bucket"
    
    [build]
      command = "npm run build"
      upload = { format = "service-worker" }
  11. Step 11

    Incremental Static Regeneration (ISR)

    Implement incremental static regeneration to update pages on-demand or based on schedules. This allows you to have mostly-static sites with fresh content.

    // src/pages/blog/[slug].astro
    import type { CollectionEntry } from 'astro:content';
    
    export async function getStaticPaths() {
      const posts = await astro.content.collection('posts').all();
      
      return posts.map((post) => ({
        params: { slug: post.slug },
        props: { post },
      }));
    }
    
    // ISR with revalidate option
    export async function GET({ params, redirect }) {
      const { slug } = await params;
      const post = await getContentBySlug(slug);
      
      if (!post) {
        redirect('/404');
      }
      
      return astro.render({ title: post.title, post });
    }
    
    // Manual revalidation via webhook
    // src/pages/api/revalidate/[slug].ts
    export async function POST(
      { params, request, context }
    ) {
      const webhookSecret = request.headers.get('x-revalidate-secret');
      if (webhookSecret !== process.env.REVALIDATE_SECRET) {
        return new Response('Unauthorized', { status: 401 });
      }
      
      await context.revalidate({
        path: `/blog/${await params}.slug`,
      });
      
      return new Response('OK');
    }
  12. Step 12

    Qwik Resumability vs React Hydration

    Understand the difference between Qwik's resumability and React's hydration. Qwik serializes state to attribute selectors and only loads code when needed, while React loads all components and hydrates them. Qwik's approach results in smaller bundle sizes and faster initial loads.

    // Qwik: Resumability pattern
    // Code is lazy-loaded only when an event occurs
    
    import { component$, $, useStore } from '@builder.io/qwik';
    
    export const Counter = component$(() => {
      const state = useStore({ count: 0 });
    
      const increment$ = $() => {
        state.count++;
      };
    
      return (
        <div>
          <p>Count: {state.count}</p>
          <button onClick$={increment$}>Increment</button>
        </div>
      );
    });
    
    // Generated HTML contains q:slot attributes for serialization
    // <div q:slot="/Counter#0"><p q:slot="/Counter#0:1">Count: 0</p></div>
    // No JavaScript loaded until user interacts
    
    // React hydration: full component tree preloaded
    import React, { useState } from 'react';
    
    function Counter() {
      const [count, setCount] = useState(0);
      
      return (
        <div>
          <p>Count: {count}</p>
          <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
      );
    }
    
    // All code must be loaded and hydrated before interaction
    ⚠ Heads up: Qwik has a steeper learning curve than React. The $() signal function and component$ are essential for proper resumability.
  13. Step 13

    Performance Monitoring and Lighthouse Scoring

    Set up performance monitoring using tools like Lighthouse CI, Web Vitals, and Cloudflare Analytics. Track Core Web Vitals metrics to ensure optimal user experience.

    {
      "ci": {
        "assert": {
          "assertion": {
            "firstContentfulPaint": { "max": 1500 },
            "largestContentfulPaint": { "max": 2500 },
            "totalBlockingTime": { "max": 300 },
            "cumulativeLayoutShift": { "max": 0.1 },
            "speedIndex": { "max": 3000 }
          }
        },
        "collect": {
          "url": "https://your-deployment.cloudflareworkers.com",
          "settings": {
            "preset": "mobile"
          },
          "numberOfRuns": 3,
          "outputPath": "./lighthouse-report.json"
        }
      }
    }
    
    // package.json
    {
      "scripts": {
        "lhci": "lhci autorun",
        "measure": "lighthouse http://localhost:4321 --view"
      }
    }
  14. Step 14

    JAMstack Project Setup Summary

    Complete your JAMstack setup by configuring environment variables, CI/CD pipelines, and monitoring dashboards for production deployment.

    # Environment variables needed
    # .env.local
    NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
    SUPABASE_ANON_KEY=your-anon-key
    CLOUDFLARE_ACCOUNT_ID=your-account-id
    CLOUDFLARE_API_TOKEN=your-api-token
    NODE_ENV=production
    
    # Deploy to Cloudflare Workers
    npx wrangler deploy
    
    # Set up CDN caching rules
    curl --request POST \
      --url https://api.cloudflare.com/client/v4/zones/{zone_id}/cache/config \
      --header 'Authorization: Bearer {api_token}' \
      --data '{"cache_rules": [{"status": 200, "ttl": 86400}]}'
  15. Step 15

    Static Asset Optimization

    Optimize static assets for the edge by using image sprites, code splitting, and CDN delivery. Astro 5 provides built-in optimizations for images, fonts, and scripts.

    // Configure Astro for image optimization
    // astro.config.mjs
    export default defineConfig({
      images: {
        domains: ['cdn.example.com'],
        service: 'sharp'
      },
      compressHTML: true,
      compressScripts: true,
      compressStyles: true,
      serverTiming: {
        enabled: true,
      },
    });
    
    // Use Astro's image component for automatic optimization
    // src/components/OptimizedImage.astro
    <script>
      import Image from 'astro:asset';
    
      interface Props {
        src: string;
        alt: string;
        width?: number;
        height?: number;
      }
    </script>
    
    <figure>
      <Image 
        src={image} 
        alt={alt} 
        width={width}
        height={height}
        quality={75}
        fetchpriority="high"
      />
      <figcaption>{alt}</figcaption>
    </figure>

Feature requests

Sign in to suggest features or vote on existing ones.

No feature requests yet.

Discussion

0 people marked this as worked·Sign in to mark your own.

Sign in to join the discussion.

No comments yet.