Next.js with Convex DB Integration
A comprehensive guide to integrating Convex DB with Next.js, including type-safe queries and mutations
Modern database integration using Convex DB with Next.js, featuring type-safe queries, mutations, and real-time updates.
Schema Definition
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
role: v.union(v.literal("USER"), v.literal("ADMIN")),
createdAt: v.number(),
}).index("by_email", ["email"]),
posts: defineTable({
title: v.string(),
content: v.string(),
authorId: v.id("users"),
published: v.boolean(),
tags: v.array(v.string()),
createdAt: v.number(),
})
.index("by_author", ["authorId"])
.index("by_published", ["published"]),
});Types
import { Doc, Id } from "./_generated/dataModel";
export type User = Doc<"users">;
export type Post = Doc<"posts">;
export type UserId = Id<"users">;
export type PostId = Id<"posts">;
export type CreateUserInput = {
name: string;
email: string;
role?: "USER" | "ADMIN";
};
export type CreatePostInput = {
title: string;
content: string;
authorId: UserId;
tags?: string[];
};Queries
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getUsers = query({
args: {
role: v.optional(v.union(v.literal("USER"), v.literal("ADMIN"))),
},
handler: async (ctx, args) => {
const users = await ctx.db
.query("users")
.filter((q) => (args.role ? q.eq(q.field("role"), args.role) : q.truthy()))
.order("desc")
.take(100);
return users;
},
});
export const getPosts = query({
args: {
authorId: v.optional(v.id("users")),
published: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
let query = ctx.db.query("posts");
if (args.authorId) {
query = query.withIndex("by_author", (q) =>
q.eq("authorId", args.authorId)
);
}
if (args.published !== undefined) {
query = query.withIndex("by_published", (q) =>
q.eq("published", args.published)
);
}
const posts = await query.order("desc").take(100);
return posts;
},
});Mutations
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const createUser = mutation({
args: {
name: v.string(),
email: v.string(),
role: v.optional(v.union(v.literal("USER"), v.literal("ADMIN"))),
},
handler: async (ctx, args) => {
const userId = await ctx.db.insert("users", {
name: args.name,
email: args.email,
role: args.role || "USER",
createdAt: Date.now(),
});
return userId;
},
});
export const createPost = mutation({
args: {
title: v.string(),
content: v.string(),
authorId: v.id("users"),
tags: v.optional(v.array(v.string())),
},
handler: async (ctx, args) => {
const postId = await ctx.db.insert("posts", {
title: args.title,
content: args.content,
authorId: args.authorId,
tags: args.tags || [],
published: false,
createdAt: Date.now(),
});
return postId;
},
});Next.js Integration
Server Component
import { getUsers } from "@/convex/queries";
export default async function UsersPage() {
const users = await getUsers({ role: "USER" });
return (
<div className="container py-8">
<h1 className="text-2xl font-bold mb-4">Users</h1>
<div className="grid gap-4">
{users.map((user) => (
<div key={user._id} className="p-4 border rounded">
<h2 className="font-semibold">{user.name}</h2>
<p className="text-gray-600">{user.email}</p>
<span className="text-sm text-gray-500">{user.role}</span>
</div>
))}
</div>
</div>
);
}Client Component with Real-time Updates
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
import { useState } from "react";
export function PostsList() {
const posts = useQuery(api.posts.getPosts, { published: true });
const createPost = useMutation(api.posts.createPost);
const [newPost, setNewPost] = useState({ title: "", content: "" });
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await createPost({
title: newPost.title,
content: newPost.content,
authorId: "YOUR_USER_ID", // Replace with actual user ID
});
setNewPost({ title: "", content: "" });
};
if (!posts) return <div>Loading...</div>;
return (
<div className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
<input
type="text"
value={newPost.title}
onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
placeholder="Post title"
className="w-full p-2 border rounded"
/>
<textarea
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
placeholder="Post content"
className="w-full p-2 border rounded"
/>
<button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded">
Create Post
</button>
</form>
<div className="grid gap-4">
{posts.map((post) => (
<div key={post._id} className="p-4 border rounded">
<h2 className="font-semibold">{post.title}</h2>
<p className="text-gray-600">{post.content}</p>
<div className="flex gap-2 mt-2">
{post.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 text-sm bg-gray-100 rounded"
>
{tag}
</span>
))}
</div>
</div>
))}
</div>
</div>
);
}Configuration
import { defineConfig } from "convex/schema";
export default defineConfig({
schema: "./schema.ts",
auth: {
providers: ["clerk"], // or other auth providers
},
});Imports
import { defineSchema, defineTable } from "convex/schema";
import { v } from "convex/values";
import { Doc, Id } from "./_generated/dataModel";
import { query, mutation } from "./_generated/server";
import { useQuery, useMutation } from "convex/react";Features:
- Type-safe database schema
- Real-time updates with Convex
- Server and client components integration
- Optimistic updates
- Indexed queries
- Proper error handling
- Authentication integration
- Clean and maintainable code structure
- Modern React patterns
- Efficient data fetching