Database
Database integration is fast and straightforward. Server Components run on the server by default, giving you direct access to databases without API routes. Server Actions let you mutate data from the client while keeping your database credentials secure.
Why This Approach Works
rari's Rust runtime delivers exceptional performance for database-heavy applications:
- Server Components by default. Query databases directly in your components without API routes
- High performance. Rust-powered runtime handles database queries efficiently
- Server Actions. Mutate data securely from client components with
'use server' - Streaming SSR. Stream database results progressively with Suspense boundaries
- React.cache() support. Deduplicate database queries across components
Neon: Recommended Database Partner
Neon is our recommended database partner for rari applications, providing Postgres with autoscaling, branching, and scale-to-zero capabilities.
Why Neon + rari
- HTTP-based connections. No connection pooling required, scales automatically
- Instant branching. Create database branches for preview deployments
- Scale to zero. Pay only for what you use
- Low latency. Optimized for fast queries worldwide
- Developer-friendly. Simple connection string, no complex setup
Quick Start with Neon
- Create a Neon account at neon.com
- Create a new project and copy your connection string
- Add it to your
.envfile:
DATABASE_URL="postgresql://user:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require"Database Clients
Any Node.js-compatible database client works here. Here are the most popular options:
Postgres Clients
Note: The Neon serverless driver uses HTTP-based connections (not TCP), which works great with rari.
ORMs and Query Builders
Querying in Server Components
Server Components can query databases directly. They're async by default, so you can await database calls right in your component.
Using Neon Serverless Driver
The Neon serverless driver uses HTTP instead of TCP, making it lightweight and ideal for modern frameworks like rari:
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
export default async function PostsPage() {
const posts = await sql`
SELECT id, title, content, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10
`
return (
<div>
<h1>Recent Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<time>{new Date(post.created_at).toLocaleDateString()}</time>
</li>
))}
</ul>
</div>
)
}Using node-postgres (pg)
import { Pool } from 'pg'
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.DATABASE_ALLOW_INSECURE_SSL === 'true'
? { rejectUnauthorized: false }
: true,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
})import { pool } from '@/lib/db'
export default async function UsersPage() {
const result = await pool.query(
'SELECT id, name, email FROM users ORDER BY created_at DESC'
)
return (
<div>
<h1>Users</h1>
<ul>
{result.rows.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
)
}Using Postgres.js
import postgres from 'postgres'
export const sql = postgres(process.env.DATABASE_URL!, {
ssl: 'require',
max: 10,
idle_timeout: 20,
connect_timeout: 10,
})import { sql } from '@/lib/db'
export default async function UsersPage() {
const users = await sql`
SELECT id, name, email FROM users
ORDER BY created_at DESC
`
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
)
}Using Drizzle ORM
import { drizzle } from 'drizzle-orm/neon-serverless'
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql)import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
})import { db } from '@/lib/db'
import { posts } from '@/lib/schema'
import { desc } from 'drizzle-orm'
export default async function PostsPage() {
const allPosts = await db
.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10)
return (
<div>
<h1>Recent Posts</h1>
<ul>
{allPosts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</li>
))}
</ul>
</div>
)
}Mutations with Server Actions
Server Actions let you mutate database data from client components while keeping your database credentials secure on the server.
Creating a Server Action
'use server'
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
// Example: Get current user from session/auth
async function getCurrentUserId() {
// Replace with your actual auth implementation
// e.g., from cookies, session, or auth library
return 1 // Placeholder
}
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
if (!title || !content) {
return { success: false, error: 'Title and content are required' }
}
try {
const userId = await getCurrentUserId()
await sql`
INSERT INTO posts (title, content, user_id)
VALUES (, , )
`
return { success: true }
} catch (error) {
console.error('Failed to create post:', error)
return { success: false, error: 'Failed to create post' }
}
}
export async function deletePost(postId: number) {
try {
const userId = await getCurrentUserId()
const result = await sql`
DELETE FROM posts
WHERE id = AND user_id =
RETURNING id
`
if (result.length === 0) {
return { success: false, error: 'Post not found or unauthorized' }
}
return { success: true }
} catch (error) {
console.error('Failed to delete post:', error)
return { success: false, error: 'Failed to delete post' }
}
}Using Server Actions in Client Components
'use client'
import { createPost } from '@/actions/posts'
import { useState } from 'react'
export default function CreatePostForm() {
const [pending, setPending] = useState(false)
async function handleSubmit(formData: FormData) {
setPending(true)
const result = await createPost(formData)
setPending(false)
if (result.success) {
// Reset form or show success message
} else {
alert(result.error)
}
}
return (
<form action={handleSubmit}>
<input
type="text"
name="title"
placeholder="Post title"
required
disabled={pending}
/>
<textarea
name="content"
placeholder="Post content"
required
disabled={pending}
/>
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
</form>
)
}API Routes with Databases
For REST APIs or webhooks, use API routes to query databases:
import { neon } from '@neondatabase/serverless'
import { ApiResponse } from 'rari'
const sql = neon(process.env.DATABASE_URL!)
// Example: Get current user from session/auth
async function getCurrentUserId() {
// Replace with your actual auth implementation
// e.g., from cookies, session, or auth library
return 1 // Placeholder
}
export async function GET(request: Request) {
try {
const url = new URL(request.url)
const parsedLimit = Number(url.searchParams.get('limit'))
const limit =
Number.isFinite(parsedLimit) && parsedLimit > 0
? Math.min(parsedLimit, 100)
: 10
const posts = await sql`
SELECT id, title, content, created_at
FROM posts
ORDER BY created_at DESC
LIMIT
`
return ApiResponse.json({ posts })
} catch (error) {
console.error('Database error - failed to fetch posts')
return ApiResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}
export async function POST(request: Request) {
try {
const userId = await getCurrentUserId()
if (!userId) {
return ApiResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const { title, content } = await request.json()
if (!title || !content) {
return ApiResponse.json(
{ error: 'Title and content are required' },
{ status: 400 }
)
}
const result = await sql`
INSERT INTO posts (title, content, user_id)
VALUES (, , )
RETURNING id, title, content, created_at
`
return ApiResponse.json({ post: result[0] }, { status: 201 })
} catch (error) {
console.error('Database error - failed to create post')
return ApiResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
)
}
}Request Deduplication
Identical fetch() requests made during the same render pass are automatically deduplicated. If multiple components fetch the same URL with the same options, only one network request is made.
For database queries and other async operations, use React.cache() to manually deduplicate calls:
import { cache } from 'react'
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
export const getUser = cache(async (userId: number) => {
const users = await sql`
SELECT id, name, email FROM users WHERE id =
`
return users[0]
})Now multiple components can call getUser(1) and only one database query will execute per request.
Streaming with Suspense
Stream database results progressively using Suspense boundaries. This lets you show a loading state while data is being fetched:
import { Suspense } from 'react'
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.DATABASE_URL!)
async function RecentPosts() {
const posts = await sql`
SELECT id, title FROM posts
ORDER BY created_at DESC
LIMIT 5
`
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
async function UserStats() {
const stats = await sql`
SELECT COUNT(*) as count FROM users
`
return <p>Total users: {stats[0].count}</p>
}
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading posts...</p>}>
<RecentPosts />
</Suspense>
<Suspense fallback={<p>Loading stats...</p>}>
<UserStats />
</Suspense>
</div>
)
}Connection Pooling
For traditional Postgres clients like pg, use connection pooling to manage database connections efficiently:
import { Pool } from 'pg'
// Create a single pool instance
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.DATABASE_ALLOW_INSECURE_SSL === 'true'
? { rejectUnauthorized: false }
: true,
max: 20, // Maximum number of clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return error after 2 seconds if no connection available
})
// Graceful shutdown
process.on('SIGTERM', async () => {
await pool.end()
})With Neon's serverless driver (@neondatabase/serverless), connection pooling is handled automatically. No configuration needed. The driver uses HTTP connections which don't require traditional pooling.
Note: By default, SSL certificate verification is enabled for security. Only set DATABASE_ALLOW_INSECURE_SSL=true in development if needed.
Environment Variables
Store database credentials in environment variables, never in your code:
# Neon Postgres
DATABASE_URL="postgresql://user:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/dbname?sslmode=require"
# Alternative: separate connection details
DB_HOST="ep-cool-darkness-123456.us-east-2.aws.neon.tech"
DB_PORT="5432"
DB_NAME="dbname"
DB_USER="user"
DB_PASSWORD="password"Add .env to your .gitignore to keep credentials out of version control.
Database Migrations
Use your ORM's migration tool or a standalone migration runner:
Drizzle Kit
# Generate migration from schema changes
npx drizzle-kit generate
# Apply migrations
npx drizzle-kit migratePrisma
# Create migration
npx prisma migrate dev --name init
# Apply migrations in production
npx prisma migrate deploynode-pg-migrate
# Create migration
npx node-pg-migrate create initial-schema
# Run migrations
npx node-pg-migrate upBest Practices
Use Server Components for Queries
Query databases directly in Server Components instead of creating API routes:
// Good: Direct database query in Server Component
export default async function PostsPage() {
const posts = await db.select().from(posts)
return <PostsList posts={posts} />
}
// Avoid: Unnecessary API route
export default async function PostsPage() {
const posts = await fetch('/api/posts').then(r => r.json())
return <PostsList posts={posts} />
}Use Server Actions for Mutations
Keep database mutations secure with Server Actions:
// Good: Server Action with 'use server'
'use server'
export async function createPost(data: FormData) {
await db.insert(posts).values({ ... })
}
// Avoid: Client-side database access
'use client'
export async function createPost() {
await db.insert(posts).values({ ... }) // Database credentials exposed!
}Handle Errors Gracefully
Always handle database errors and provide fallbacks:
export default async function PostsPage() {
try {
const posts = await db.select().from(posts)
return <PostsList posts={posts} />
} catch (error) {
console.error('Database error - failed to load posts')
return <p>Failed to load posts. Please try again later.</p>
}
}Use Prepared Statements
Prevent SQL injection by using parameterized queries. Tagged template literals automatically escape parameters, but avoid manual string concatenation:
// Good: Parameterized query with tagged template
await sql`SELECT * FROM users WHERE id = `
// Dangerous: Manual string concatenation bypasses escaping
const query = "SELECT * FROM users WHERE id = '" + userId + "'"
await sql(query) // SQL injection risk!Cache Expensive Queries
Use React.cache() for queries called multiple times per request:
import { cache } from 'react'
export const getSettings = cache(async () => {
return await db.select().from(settings)
})Monitor Query Performance
Log slow queries in development to identify performance bottlenecks:
const start = Date.now()
const result = await db.select().from(posts)
const duration = Date.now() - start
if (duration > 100) {
console.warn(`Slow query: ms`)
}Other Databases
Any database with a Node.js client will work:
MySQL
import mysql from 'mysql2/promise'
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
})
export default async function UsersPage() {
const [rows] = await pool.query('SELECT * FROM users')
return <UsersList users={rows} />
}MongoDB
import { MongoClient } from 'mongodb'
const client = new MongoClient(process.env.MONGODB_URI!)
const db = client.db('myapp')
export default async function PostsPage() {
const posts = await db.collection('posts').find().toArray()
return <PostsList posts={posts} />
}Note: In production, use a singleton pattern to reuse the MongoClient connection across requests. Creating a new client at module scope works, but you should handle connection lifecycle properly. Consider using a connection helper that caches the client promise and handles client.connect() and graceful shutdown.
SQLite
// better-sqlite3 is synchronous - no async/await needed
import Database from 'better-sqlite3'
const db = new Database('myapp.db')
export default function PostsPage() {
const posts = db.prepare('SELECT * FROM posts').all()
return <PostsList posts={posts} />
}