A full-stack Next.js 15 application demonstrating drizzle-cube integration with App Router and a complete analytics dashboard.
See this example running live with the full dashboard and query builder functionality.
- Next.js 15 App Router: Modern React patterns with server components and API routes
- drizzle-cube Integration: Semantic layer with Cube.js-compatible API endpoints
- PostgreSQL Database: Self-contained with Docker
- Analytics Dashboard: Interactive charts with drag-and-drop editing
- Query Builder: Real-time query construction and execution
- TypeScript: Full type safety from database to UI
# Install dependencies
npm install
# Setup database and seed data
npm run setup
# Start Next.js development server (port 6001)
npm run dev
Visit:
- Frontend Dashboard: http://localhost:6001
- Cube API: http://localhost:6001/api/cubejs-api/v1/meta
This example uses Next.js 15's App Router for a modern full-stack architecture:
- API Routes:
app/api/cube/[...path]/route.ts
handles all Cube.js endpoints - Client Components: React components with
'use client'
for interactivity - Server Components: Default server-side rendering for optimal performance
- Database Integration: Direct Drizzle ORM connection in API routes
examples/nextjs/
βββ app/
β βββ layout.tsx # Root layout with global styles
β βββ page.tsx # Main page with tab navigation
β βββ globals.css # Global styles with drizzle-cube imports
β βββ api/cube/[...path]/
β βββ route.ts # Cube.js API endpoints
βββ components/
β βββ DashboardTab.tsx # Dashboard with localStorage persistence
β βββ QueryBuilderTab.tsx # Interactive query builder
βββ lib/
β βββ db.ts # Database connection
β βββ dashboard-config.ts # Default dashboard configuration
βββ schema.ts # Database schema (Drizzle ORM)
βββ cubes.ts # Cube definitions
βββ docker-compose.yml # PostgreSQL (port 54924)
The catch-all route handler provides these Cube.js-compatible endpoints:
GET/POST /api/cubejs-api/v1/load
- Execute queriesGET /api/cubejs-api/v1/meta
- Get cube metadataGET/POST /api/cubejs-api/v1/sql
- Generate SQLGET/POST /api/cubejs-api/v1/dry-run
- Validate queries
- Port: 54924 (unique to avoid conflicts)
- Container:
drizzle-cube-nextjs-postgres
- Data: Employee, department, and productivity analytics
- Migrations: Drizzle Kit for schema management
# Development
npm run dev # Start Next.js development server (port 6001)
npm run build # Build for production
npm run start # Run production build
npm run lint # Run ESLint
npm run type-check # TypeScript type checking
# Database
npm run setup # Full setup (Docker + migrate + seed)
npm run docker:up # Start PostgreSQL container
npm run docker:down # Stop PostgreSQL container
npm run docker:reset # Reset database (delete all data)
npm run db:migrate # Run database migrations
npm run db:seed # Seed sample data
npm run db:generate # Generate new migrations
The example includes a fully interactive 4-chart dashboard:
- Employees by Department (Bar Chart)
- Productivity Trend (Line Chart - Last 30 Days)
- Team Happiness Distribution (Pie Chart)
- Productivity by Department (Bar Chart)
- Edit Mode: Click "Edit Dashboard" to enable layout editing
- Drag & Drop: Rearrange charts by dragging titles
- Resize: Use corner handles to resize charts
- Chart Settings: Edit individual chart configurations
- Auto-Save: Changes automatically saved to localStorage
- Reset: Restore default layout and configuration
This example uses localStorage for dashboard customization:
- Simple demo implementation for quick testing
- User-specific layouts persist across browser sessions
- Easy to replace with database storage for production
For production applications, implement server-side persistence:
// Replace localStorage with API calls
const saveDashboard = async (config) => {
await fetch('/api/dashboards', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config })
})
}
Interactive query construction with:
- Measures: Select count, sum, average aggregations
- Dimensions: Choose grouping fields
- Time Dimensions: Date-based analysis with granularity
- Filters: Apply conditions to limit results
- Live Execution: See results as you build queries
- Schema Exploration: Browse available cubes and fields
# Get cube metadata
curl http://localhost:6001/api/cubejs-api/v1/meta
# Execute a query
curl -X POST http://localhost:6001/api/cubejs-api/v1/load \
-H "Content-Type: application/json" \
-d '{
"measures": ["Employees.count"],
"dimensions": ["Departments.name"],
"cubes": ["Employees", "Departments"]
}'
# Generate SQL for a query
curl -X POST http://localhost:6001/api/cubejs-api/v1/sql \
-H "Content-Type: application/json" \
-d '{
"measures": ["Productivity.avgLinesOfCode"],
"timeDimensions": [{
"dimension": "Productivity.date",
"granularity": "day"
}]
}'
This example demonstrates modern Next.js patterns:
// Server Component (default)
export default function Layout({ children }) {
return <html><body>{children}</body></html>
}
// Client Component (interactive)
'use client'
export default function DashboardTab() {
const [config, setConfig] = useState(defaultConfig)
// ... interactive logic
}
// API Route (catch-all)
export async function GET(request, { params }) {
// Handle dynamic routes
}
Full type safety from database to UI:
- Database: Drizzle ORM schema with TypeScript types
- API: Next.js adapter with typed security context
- Client: React components with proper type inference
- Configuration: Strict TypeScript compilation
- Server Components: Default server-side rendering
- Client Components: Only where interactivity is needed
- Static Generation: Build-time optimization for charts
- API Routes: Efficient database connection pooling
- Update Dashboard Config (
lib/dashboard-config.ts
):
export const dashboardConfig = {
portlets: [
// ... existing charts
{
id: 'new-chart',
title: 'New Chart Title',
query: JSON.stringify({
measures: ['YourCube.measure'],
dimensions: ['YourCube.dimension']
}),
chartType: 'bar', // or 'line', 'pie', 'area'
x: 0, y: 8, w: 6, h: 4 // Grid position
}
]
}
- Define Cube (
cubes.ts
):
export const newCube = defineCube(schema, {
name: 'NewCube',
sql: ({ db, securityContext }) =>
db.select()
.from(schema.newTable)
.where(eq(schema.newTable.organisationId, securityContext.organisationId)),
measures: {
count: { sql: schema.newTable.id, type: 'count' }
},
dimensions: {
name: { sql: schema.newTable.name, type: 'string' }
}
})
- Register Cube (
cubes.ts
):
export const allCubes = [existingCubes, newCube]
For production applications, implement real authentication:
// app/api/cube/[...path]/route.ts
const extractSecurityContext = async (request: NextRequest) => {
// Extract JWT from Authorization header
const token = request.headers.get('Authorization')?.replace('Bearer ', '')
const decoded = await verifyJWT(token)
return {
organisationId: decoded.orgId,
userId: decoded.userId,
roles: decoded.roles
}
}
The example uses Tailwind CSS with drizzle-cube styles:
- Global Styles:
app/globals.css
imports drizzle-cube CSS - Components: Tailwind classes for responsive design
- Charts: Recharts with customizable themes
- Grid Layout: react-grid-layout for dashboard editing
This Next.js example provides enhanced functionality:
- App Router: Modern React patterns with server components
- Full-Stack: API routes integrated with frontend
- TypeScript: Strict type checking throughout
- Performance: Server-side rendering and optimization
- Developer Experience: Hot reload for both API and UI
- Same Database Schema: Identical to Express example
- Same Cubes: Exact same analytics definitions
- Same Dashboard: 4-chart layout with editing
- Same API: Cube.js-compatible endpoints
- localStorage: Dashboard persistence for demo
- Single Application: No separate server/client builds
- Unified Configuration: One package.json, one Docker setup
- API Integration: No proxy configuration needed
- Static Export: Can build for static hosting
Replace localStorage with proper database storage:
// Add to schema.ts
export const dashboards = pgTable('dashboards', {
id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
name: text('name').notNull(),
config: jsonb('config').notNull(),
userId: integer('user_id').notNull(),
organisationId: integer('organisation_id').notNull(),
createdAt: timestamp('created_at').defaultNow()
})
Implement proper security:
- JWT token validation
- Role-based access control
- Multi-tenant data isolation
- Rate limiting and CORS
Optimize for production:
- Enable Next.js static generation
- Implement proper caching strategies
- Use CDN for static assets
- Database connection pooling
Add observability:
- Error tracking (Sentry)
- Performance monitoring
- Database query optimization
- API usage analytics
- Port Conflicts: Ensure port 54924 is available for PostgreSQL
- Database Connection: Check Docker container is running
- Type Errors: Run
npm run type-check
for TypeScript issues - CSS Not Loading: Verify drizzle-cube styles import in globals.css
- Use
npm run docker:reset
to clean database state - Check browser console for client-side errors
- Use Next.js DevTools for debugging
- Monitor API calls in Network tab
Perfect for learning drizzle-cube with modern Next.js patterns and building production-ready analytics applications!