Clone
Migrating QRurl to Nuxt 3 with NuxtFlare
Why Nuxt 3 + NuxtFlare?
Current Architecture Problems:
- Separate frontend (Vue) and backend (Workers) codebases
- Complex CORS configuration
- Manual static file serving
- Two different routing systems
- Complicated deployment process
Nuxt 3 + NuxtFlare Benefits:
- Single codebase - Frontend and backend unified
- SSR on Workers - Better SEO and performance
- Built-in API routes -
/server/api/*routes - Direct Cloudflare bindings - D1, R2, KV without wrangler
- Zero CORS issues - Everything on same origin
- Simple deployment -
npm run deploy - Hot module replacement - Full-stack HMR in development
- TypeScript support - End-to-end type safety
Migration Strategy
Phase 1: Setup Nuxt 3 Project
# Create new Nuxt 3 app
npx nuxi@latest init qrurl-nuxt
# Install dependencies
cd qrurl-nuxt
npm install
# Add NuxtFlare module
npm install @nuxflare/core
Phase 2: Configure NuxtFlare
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxflare/core'],
nitro: {
preset: 'cloudflare-pages',
// Cloudflare bindings
cloudflare: {
bindings: {
DB: {
type: 'd1',
databaseName: 'qrurl-db',
databaseId: '17eb6fdb-19da-4ed7-931c-a4cdef281f8c'
},
STORAGE: {
type: 'r2',
bucketName: 'qrurl-storage'
},
CACHE: {
type: 'kv',
namespaceId: '1cacb0f1b44b4324b62c1bc010ff15f5'
}
}
}
}
})
Phase 3: Migrate Components
Move Vue components to Nuxt structure:
frontend/src/components/* → components/
frontend/src/views/* → pages/
frontend/src/stores/* → stores/ (with Pinia)
Phase 4: Convert API Routes
Transform Workers routes to Nuxt server routes:
Before (Workers):
// src/routes/api.js
export async function apiRoutes(request, env, ctx) {
if (path === '/api/links') {
return getLinks(request, env);
}
}
After (Nuxt):
// server/api/links.get.ts
export default defineEventHandler(async (event) => {
const db = useD1Database('DB', event);
return await db.prepare('SELECT * FROM links').all();
})
Phase 5: Authentication with Nuxt
// server/api/auth/request.post.ts
import { usePostmark } from '#nuxflare/postmark';
export default defineEventHandler(async (event) => {
const { email } = await readBody(event);
const postmark = usePostmark(event);
// Generate magic link
const token = generateToken();
// Store in D1
const db = useD1Database('DB', event);
await db.prepare('INSERT INTO auth_tokens...').run();
// Send email
await postmark.sendEmail({
From: 'noreply@qrurl.us',
To: email,
Subject: 'Your QRurl Login Link',
HtmlBody: `...`
});
return { success: true };
})
Phase 6: File Uploads with R2
// server/api/logo/upload.post.ts
export default defineEventHandler(async (event) => {
const form = await readFormData(event);
const file = form.get('logo') as File;
const r2 = useR2Bucket('STORAGE', event);
await r2.put(`logos/${file.name}`, file);
return { url: `/api/logo/${file.name}` };
})
File Structure (Nuxt)
qrurl-nuxt/
├── server/
│ ├── api/
│ │ ├── auth/
│ │ │ ├── request.post.ts
│ │ │ └── verify.post.ts
│ │ ├── links/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ └── [id].delete.ts
│ │ └── logo/
│ │ ├── upload.post.ts
│ │ └── [id].get.ts
│ └── routes/
│ └── [slug].ts # Short link redirects
├── pages/
│ ├── index.vue # Homepage
│ ├── dashboard.vue # Dashboard
│ └── auth/
│ └── verify.vue # Magic link verification
├── components/
│ ├── NavBar.vue
│ ├── CreateLinkForm.vue
│ ├── QRCodeGenerator.vue
│ └── LogoUploader.vue
├── stores/
│ └── auth.ts # Pinia store
├── nuxt.config.ts
└── wrangler.toml # Minimal, just for secrets
Deployment (Simplified!)
Development:
npm run dev
# That's it! Full-stack hot reload
Production:
npm run build
npm run deploy
# Deploys everything to Cloudflare Workers/Pages
Benefits Over Current Architecture
| Feature | Current (Vue + Workers) | Nuxt 3 + NuxtFlare |
|---|---|---|
| Codebase | 2 separate | 1 unified |
| Routing | Vue Router + Workers Router | Nuxt file-based routing |
| API | Manual Workers setup | Built-in server routes |
| SSR | No (SPA only) | Yes (better SEO) |
| Development | 2 servers + CORS | 1 server, no CORS |
| Type Safety | Partial | Full-stack TypeScript |
| Deployment | Complex scripts | Single command |
| HMR | Frontend only | Full-stack |
| Data Fetching | Manual fetch + CORS | $fetch with auto-typing |
Migration Timeline
- Week 1: Set up Nuxt project, migrate components
- Week 2: Convert API routes to server routes
- Week 3: Integrate D1, R2, KV with NuxtFlare
- Week 4: Testing and deployment optimization
Example: Complete Link Creation Flow
<!-- pages/dashboard.vue -->
<template>
<form @submit.prevent="createLink">
<input v-model="url" />
<button type="submit">Shorten</button>
</form>
</template>
<script setup>
const url = ref('');
async function createLink() {
// No CORS, no API URL config needed!
const { slug } = await $fetch('/api/links', {
method: 'POST',
body: { url: url.value }
});
// Redirect to success page
await navigateTo(`/link/${slug}`);
}
</script>
// server/api/links.post.ts
export default defineEventHandler(async (event) => {
const { url } = await readBody(event);
const db = useD1Database('DB', event);
const slug = generateSlug();
await db.prepare(
'INSERT INTO links (slug, url) VALUES (?, ?)'
).bind(slug, url).run();
return { slug };
})
Conclusion
Migrating to Nuxt 3 with NuxtFlare would:
- Eliminate complexity - No more CORS, separate deployments, or complex configs
- Improve developer experience - Single codebase, full-stack HMR
- Enhance performance - SSR, edge rendering, better caching
- Simplify deployment - One command to deploy everything
The migration effort would be worth it for the massive simplification and improved developer experience.