Deploying Nuxt 4 + Nuxt Content v3 on Vercel with API Routes

Neha Keshri
Web Developer
TL;DR: Deploying a Nuxt 4 portfolio with Nuxt Content v3 and API routes on Vercel requires hybrid rendering with better-sqlite3 in devDependencies. Read on for the complete debugging journey. Yes, this is the story of this website.
The Project
A portfolio website built with:
- Nuxt 4 (latest stable)
- Nuxt Content v3 for blog posts and project pages
- Server API routes for a contact form (
/server/api/contact.post.ts) - Vercel as the deployment target
The goal was straightforward: deploy a portfolio site with dynamic content management and a working contact form. What followed was a three-stage debugging journey that exposed the nuances of serverless deployments, native Node.js modules, and hybrid rendering.
Issue #1: The 500 Error - Native Binary Mismatch
What Happened
After deploying to Vercel using the standard nuxt build command, all content pages (/blog/test, /work/project-name, etc.) returned 500 Internal Server Error. The Vercel logs showed:
[error] Failed to execute SQL CREATE TABLE IF NOT EXISTS _content_info...
Module did not self-register: '/var/task/node_modules/better-sqlite3/build/Release/better_sqlite3.node'.
H3Error: Module did not self-register: '/var/task/node_modules/better-sqlite3/build/Release/better_sqlite3.node'.
cause: Error: Module did not self-register...
code: 'ERR_DLOPEN_FAILED'
Initial Setup
The package.json had better-sqlite3 as a direct dependency:
{
"dependencies": {
"@nuxt/content": "^3.11.2",
"better-sqlite3": "^12.6.2",
"nuxt": "^4.3.1",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
}
}
Root Cause Analysis
What is better-sqlite3?
- A native Node.js addon (compiled C++ code)
- Produces a
.nodebinary file duringnpm install - The binary is platform-specific (OS + architecture + Node.js version)
Why did it fail?
- Vercel's build environment compiled the binary for one environment
- Vercel's Lambda runtime (where the code actually runs) is a different environment
- The pre-compiled binary was incompatible with the Lambda runtime
- Node.js threw
ERR_DLOPEN_FAILEDwhen trying to load it
The mistake: Manually adding better-sqlite3 as a dependency overrode Nuxt Content's internal database adapter selection logic, which is designed to choose the right adapter based on the deployment target.
First Solution Attempt
Action taken: Remove better-sqlite3 from dependencies and switch to static generation.
{
"dependencies": {
"@nuxt/content": "^3.11.2",
"nuxt": "^4.3.1",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
}
}
Vercel settings:
- Build command:
npm run generate - Output directory:
.output/public
Why this worked (initially):
nuxt generatepre-renders all pages to static HTML at build time- Content is processed during the build (where SQLite works fine)
- Runtime deployment is just static files on Vercel's CDN
- No server, no SQLite, no native binary issues
Result: ✅ Content pages worked perfectly. Site deployed successfully.
Issue #2: The Missing API - Static vs Dynamic
What Happened
After switching to static generation, the contact form started returning 404 Not Found errors when submitting. The API endpoint /api/contact was completely unavailable.
Root Cause Analysis
The fundamental trade-off:
nuxt generatecreates a purely static site — just HTML, CSS, and JavaScript files- Server routes (
/server/api/*) are not included in static builds - They require a Node.js runtime to execute, which static hosting doesn't provide
The architecture mismatch:
Static Generation:
├── Pre-render: Everything → HTML files
├── Deploy: Only static files
└── Runtime: No server, no API routes ❌
Server-Side Rendering:
├── Build: Server bundle + client bundle
├── Deploy: Server runs on every request
└── Runtime: SQLite needed ❌ (back to Issue #1)
The Real Requirement
The project needed hybrid rendering:
- Content pages: Static (fast, no SQLite)
- API routes: Dynamic (serverless functions)
Issue #3: The Stuck Build - Timing Matters
What Happened
Switched back to nuxt build with hybrid rendering configuration:
export default defineNuxtConfig({
nitro: {
preset: 'vercel',
},
routeRules: {
'/': { prerender: true },
'/blog/**': { prerender: true },
'/work/**': { prerender: true },
'/project/**': { prerender: true },
'/api/**': { cors: true },
}
})
Vercel settings:
- Build command:
npm run build - Output directory:
.output
But the build got stuck with this prompt:
> postinstall
> nuxt prepare
[error] [@nuxt/content] Nuxt Content requires `better-sqlite3` module to operate.
❯ Do you want to install `better-sqlite3` package?
● Yes / ○ No
The build hung indefinitely, waiting for user input that couldn't be provided in a CI environment.
Root Cause Analysis
The lifecycle issue:
- Vercel runs
npm install - This triggers the
postinstallscript:nuxt prepare - During
nuxt prepare, Nuxt Content checks forbetter-sqlite3 - Since it's not in
dependencies, Content prompts to install it - In a non-interactive CI environment, the build hangs forever
Why the prompt appeared:
- Nuxt Content needs
better-sqlite3at build time to index markdown files - Even with hybrid rendering, the prerendered routes still need content indexing during the build
- The module wasn't available, triggering the installation prompt
Final Solution
The key insight: better-sqlite3 is needed at build time but not at runtime.
In npm/Node.js dependency management:
dependencies: Installed in both development and productiondevDependencies: Installed only in development and during builds
The fix:
{
"dependencies": {
"@nuxt/content": "^3.11.2",
"nuxt": "^4.3.1",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
},
"devDependencies": {
"better-sqlite3": "^12.6.2"
}
}
Why this works:
- Build time (Vercel's build machine):
- Installs both
dependenciesanddevDependencies better-sqlite3is available for Nuxt Content to index files- Prerendered routes are generated successfully
- No prompts, no hanging
- Installs both
- Runtime (Vercel's Lambda functions):
- Only
dependenciesare bundled into serverless functions better-sqlite3is not included- Prerendered pages don't need it (they're just HTML)
- API routes run independently without needing SQLite
- Only
Result: ✅ Build completes, content pages load, API routes work.
Final Working Configuration
package.json
{
"name": "portfolio",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/content": "^3.11.2",
"nuxt": "^4.3.1",
"vue": "^3.5.28",
"vue-router": "^4.6.4"
},
"devDependencies": {
"better-sqlite3": "^12.6.2"
}
}
nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/content'],
nitro: {
preset: 'vercel',
},
routeRules: {
// Pre-render content pages at build time
'/': { prerender: true },
'/blog': { prerender: true },
'/blog/**': { prerender: true },
'/work': { prerender: true },
'/work/**': { prerender: true },
'/project': { prerender: true },
'/project/**': { prerender: true },
// Keep API routes as serverless functions
'/api/**': { cors: true },
}
})
Vercel Settings
- Build Command:
npm run build - Output Directory:
.output(or leave empty for auto-detection) - Node.js Version: 18.x or higher
Key Takeaways
1. Native Modules Are Platform-Specific
Native Node.js addons like better-sqlite3 compile to binaries that only work on specific platforms. Serverless environments like Vercel's Lambda have different runtimes than build machines, causing incompatibility.
2. Dependencies vs DevDependencies Matter in Serverless
In traditional server deployments, this distinction is minor. In serverless:
- Build environment: Full access to both dependency types
- Runtime environment: Only
dependenciesare bundled - Build-only tools should be in
devDependencies
3. Hybrid Rendering Is the Sweet Spot
For content sites with API routes:
- Don't use pure SSR (requires runtime SQLite)
- Don't use pure static (loses API routes)
- Use hybrid rendering with route rules
4. Nuxt Content's Documentation Assumes Context
When the docs say "no extra config needed for Vercel," they assume:
- You're using
nuxt generatefor pure static, OR - You're using hybrid rendering with proper dependency management
- They don't mean "just run
nuxt buildwith SQLite in dependencies"
5. The Build Lifecycle Matters
Understanding when each step runs:
npm install→ Installsdependencies+devDependenciespostinstall→ Runsnuxt prepare(needs build tools)- Build command → Runs
nuxt build(needs build tools) - Deploy → Only
dependenciespackaged - Runtime → Serverless functions execute
Debugging Checklist for Similar Issues
If you encounter deployment issues with Nuxt Content on Vercel:
- Is
better-sqlite3in the right place? (devDependencies) - Are you using the correct preset? (
vercel, notvercel-static) - Are content routes prerendered in routeRules?
- Are API routes excluded from prerendering?
- Is the build command
nuxt build? (notnuxt generate) - Is the output directory
.output? (not.output/public) - Did you run
npm installafter moving dependencies? - Are there any broken internal links causing prerender failures?
Alternative Approaches Considered
Option A: Use a Different Database Adapter
Nuxt Content can theoretically use other adapters, but they're not officially documented or recommended. Sticking with the designed workflow is safer.
Option B: Disable Content Indexing
Not viable — it breaks the entire content querying system that Nuxt Content provides.
Option C: Use Edge Runtime
Vercel Edge Runtime doesn't support native modules at all. Would require a complete rearchitecture.
Option D: Self-Host on a Traditional VPS
Would work fine, but defeats the purpose of using Vercel's serverless infrastructure and CDN benefits.
Conclusion
Deploying Nuxt Content v3 on Vercel with API routes requires understanding the intersection of:
- Native Node.js modules and platform compatibility
- Build-time vs runtime dependency management
- Hybrid rendering with selective prerendering
- Serverless deployment architectures
The final solution is elegant: better-sqlite3 in devDependencies, hybrid rendering with route rules, and the Vercel preset. This gives you:
- Fast, CDN-served content pages
- Working API routes as serverless functions
- No native binary issues in production
- Automatic redeployment on content changes
The key is letting Nuxt Content do what it's designed to do — handle the database layer intelligently based on context — while providing it the build-time tools it needs without polluting the runtime bundle.