π Hanalei App β Fullstack Setup with Mongoose, React Query, shadcn/ui, and TanStack Table
π§° Tech Stackβ
- Next.js (App Router + TypeScript)
- Tailwind CSS v4
- React Hook Form
- Mongoose + MongoDB
- React Query (scoped to messages layout)
- shadcn/ui components
- TanStack Table logic for data grid
β Step 1: Create the Projectβ
npx create-next-app@latest hanalei-next-app \
--typescript \
--tailwind \
--app \
--src-dir \
--import-alias "@/*"
cd hanalei-next-app
β Step 2: Install Packagesβ
npm install react-hook-form mongoose @tanstack/react-query @tanstack/react-table
β Step 3: Tailwind CSS Setupβ
Already configured by create-next-app
.
β Step 4: Create Pagesβ
Home Page (src/app/page.tsx
)β
import Link from 'next/link';
export default function HomePage() {
return (
<div className="text-center p-10">
<h1 className="text-3xl font-bold">Welcome to My Site</h1>
<p className="mt-4">Helping you get started quickly.</p>
<Link href="/contact">
<button className="mt-6 bg-blue-500 text-white px-4 py-2 rounded">
Contact Us
</button>
</Link>
</div>
);
}
Contact Page (src/app/contact/page.tsx
)β
'use client';
import { useForm } from 'react-hook-form';
export default function ContactPage() {
const { register, handleSubmit, reset } = useForm();
const onSubmit = async (data: any) => {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (res.ok) {
alert("Message saved!");
reset();
} else {
alert("Something went wrong.");
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="max-w-md mx-auto p-6 bg-white rounded shadow mt-10">
<input {...register("name", { required: true })} placeholder="Name" className="w-full border p-2 mb-4" />
<input {...register("email", { required: true })} placeholder="Email" className="w-full border p-2 mb-4" />
<textarea {...register("message", { required: true })} placeholder="Message" className="w-full border p-2 mb-4" />
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">Send</button>
</form>
);
}
β Step 5: MongoDB + Mongoose Setupβ
.env.local
β
MONGODB_URI=mongodb+srv://<user>:<pass>@cluster0.mongodb.net/hanalei?retryWrites=true&w=majority
src/models/Message.ts
β
import mongoose, { Schema, models, model } from 'mongoose';
const messageSchema = new Schema({
name: String,
email: String,
message: String,
}, { timestamps: true });
export const Message = models.Message || model('Message', messageSchema);
src/lib/mongoose.ts
β
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI!;
if (!MONGODB_URI) throw new Error('Missing MONGODB_URI');
let cached = (global as any).mongoose || (global as any).mongoose = { conn: null, promise: null };
export default async function dbConnect() {
if (cached.conn) return cached.conn;
if (!cached.promise) {
cached.promise = mongoose.connect(MONGODB_URI, { bufferCommands: false });
}
cached.conn = await cached.promise;
return cached.conn;
}
β Step 6: API Routeβ
src/app/api/contact/route.ts
β
import { NextResponse } from 'next/server';
import dbConnect from '@/lib/mongoose';
import { Message } from '@/models/Message';
export async function POST(req: Request) {
try {
const body = await req.json();
await dbConnect();
const savedMessage = await Message.create(body);
return NextResponse.json({ success: true, id: savedMessage._id });
} catch {
return NextResponse.json({ success: false }, { status: 500 });
}
}
export async function GET() {
try {
await dbConnect();
const messages = await Message.find().sort({ createdAt: -1 });
return NextResponse.json({ messages });
} catch {
return NextResponse.json({ messages: [] }, { status: 500 });
}
}
β
Step 7: React Query Scoped to /messages
β
src/app/messages/layout.tsx
β
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default function MessagesLayout({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
β Step 8: Set Up Data Tableβ
Install base table UI from shadcn:β
npx shadcn@latest add table
Create src/components/ui/data-table.tsx
β
'use client';
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
}
Create src/components/ui/columns.ts
β
import { ColumnDef } from '@tanstack/react-table';
export type Message = {
_id: string;
name: string;
email: string;
message: string;
};
export const columns: ColumnDef<Message>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'message', header: 'Message' },
];
β Step 9: Use the Table in Pageβ
src/app/messages/page.tsx
β
'use client';
import { useQuery } from '@tanstack/react-query';
import { columns } from '@/components/ui/columns';
import { DataTable } from '@/components/ui/data-table';
async function fetchMessages() {
const res = await fetch('/api/contact');
return res.json();
}
export default function MessagesPage() {
const { data, isLoading } = useQuery({
queryKey: ['messages'],
queryFn: fetchMessages,
});
return (
<div className="p-6">
<h1 className="text-2xl font-semibold mb-4">Messages</h1>
{!isLoading && data ? (
<DataTable columns={columns} data={data.messages} />
) : (
<p>Loading...</p>
)}
</div>
);
}
β Step 10: Initialize Sanity Studio (Optional)β
npm install -g sanity
sanity init
Choose the "Clean project" template and dataset: production
- Create
sanity/schemaTypes/homepage.js
:
import { defineField, defineType } from 'sanity';
export const homepage = defineType({
name: 'homepage',
title: 'Homepage',
type: 'document',
fields: [
defineField({ name: 'title', type: 'string' }),
defineField({ name: 'subtitle', type: 'string' }),
defineField({ name: 'ctaText', type: 'string' }),
defineField({ name: 'ctaLink', type: 'string' }),
defineField({ name: 'image', type: 'image' }),
]
});
- Edit
sanity/schemaTypes/index.js
:
import { homepage } from './homepage';
export const schemaTypes = [homepage];
- Run the studio:
sanity dev
8. Connect Sanity to Frontend and Configure CORSβ
- Create
src/sanityClient.js
:
import sanityClient from '@sanity/client';
export const client = sanityClient({
projectId: 'your_project_id_here',
dataset: 'production',
useCdn: true,
apiVersion: '2023-01-01'
});
-
Find your Project ID:
- In
sanity.config.js
- Or at https://www.sanity.io/manage
- In
-
Configure CORS:
- Visit https://www.sanity.io/manage
- Go to API settings β CORS Origins
- Add:
http://localhost:5173
- Leave βAllow credentialsβ unchecked and Save
9. Update Homepage to Use Sanityβ
Replace Home.jsx
with:
import React, { useEffect, useState } from 'react';
import { client } from '../sanityClient';
import { Link } from 'react-router-dom';
export default function Home() {
const [content, setContent] = useState({});
useEffect(() => {
client.fetch(`*[_type == "homepage"][0]`).then(setContent);
}, []);
return (
<div className="text-center p-10">
<h1 className="text-3xl font-bold">{content.title || "Loading..."}</h1>
<p className="mt-4">{content.subtitle}</p>
<Link to={content.ctaLink || "/contact"}>
<button className="mt-6 bg-blue-500 text-white px-4 py-2 rounded">
{content.ctaText || "Contact Us"}
</button>
</Link>
</div>
);
}
β You're now running a complete fullstack app!