| name | server-setup |
| description | Set up drizzle-cube API server with Express, Fastify, Hono, or Next.js framework adapters. Use when configuring the semantic layer server, setting up API endpoints, extracting security context, or initializing drizzle-cube with different web frameworks. |
Drizzle Cube Server Setup
This skill helps you set up a Drizzle Cube API server using framework adapters for Express, Fastify, Hono, or Next.js. These adapters provide Cube.js-compatible API endpoints for your semantic layer.
Core Concept
Drizzle Cube provides framework adapters that:
- Expose Cube.js-compatible REST API endpoints
- Handle security context extraction from requests
- Integrate with your existing web framework
- Support
/load,/sql, and/metaendpoints - Create the semantic layer compiler internally from cubes array
Express Adapter
Installation
npm install express drizzle-cube
Basic Setup
import express from 'express'
import { createCubeRouter } from 'drizzle-cube/adapters/express'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
// Initialize database connection
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const app = express()
app.use(express.json())
// Create Cube API router with cubes array
const cubeRouter = createCubeRouter({
cubes: [employeesCube, departmentsCube], // Array of cube definitions
drizzle: db,
schema: schema,
// Extract security context from request
extractSecurityContext: async (req, res) => {
// Example: Extract from authenticated user
return {
organisationId: req.user?.organisationId || 'default-org',
userId: req.user?.id
}
}
})
// Mount the router
app.use('/cubejs-api/v1', cubeRouter)
// Start server
app.listen(3000, () => {
console.log('Drizzle Cube API listening on port 3000')
})
With Authentication Middleware
import express from 'express'
import { createCubeRouter } from 'drizzle-cube/adapters/express'
import { authenticateJWT } from './auth'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const app = express()
app.use(express.json())
// Authentication middleware
app.use('/cubejs-api', authenticateJWT)
const cubeRouter = createCubeRouter({
cubes: [employeesCube, departmentsCube],
drizzle: db,
schema: schema,
extractSecurityContext: async (req, res) => {
// req.user is populated by authenticateJWT middleware
if (!req.user) {
throw new Error('Unauthorized: No user found')
}
return {
organisationId: req.user.organisationId,
userId: req.user.id,
role: req.user.role
}
},
engineType: 'postgres', // Optional: specify database type
cors: { origin: 'https://yourdomain.com' } // Optional: CORS configuration
})
app.use('/cubejs-api/v1', cubeRouter)
app.listen(3000)
Express Adapter Options
interface ExpressAdapterOptions {
cubes: Cube[] // REQUIRED: Array of cube definitions
drizzle: DrizzleDatabase // REQUIRED: Drizzle database instance
schema?: any // RECOMMENDED: Database schema
extractSecurityContext: (req, res) => SecurityContext | Promise<SecurityContext> // REQUIRED
engineType?: 'postgres' | 'mysql' | 'sqlite' // Optional: auto-detected if not provided
cors?: CorsOptions // Optional: CORS configuration
basePath?: string // Optional: API base path
jsonLimit?: string // Optional: JSON body parser limit (default: '10mb')
}
Fastify Adapter
Installation
npm install fastify drizzle-cube
Basic Setup
import Fastify from 'fastify'
import { registerCubeRoutes } from 'drizzle-cube/adapters/fastify'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const fastify = Fastify({
logger: true
})
// Register Cube API plugin
await registerCubeRoutes(fastify, {
cubes: [employeesCube, departmentsCube], // Array of cube definitions
drizzle: db,
schema: schema,
extractSecurityContext: async (request) => {
// Extract from Fastify request
return {
organisationId: request.user?.organisationId || 'default-org',
userId: request.user?.id
}
}
})
// Start server
await fastify.listen({ port: 3000 })
With JWT Authentication
import Fastify from 'fastify'
import fastifyJWT from '@fastify/jwt'
import { registerCubeRoutes } from 'drizzle-cube/adapters/fastify'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const fastify = Fastify({ logger: true })
// Register JWT plugin
await fastify.register(fastifyJWT, {
secret: process.env.JWT_SECRET!
})
// Authentication decorator
fastify.decorate('authenticate', async (request, reply) => {
try {
await request.jwtVerify()
} catch (err) {
reply.send(err)
}
})
// Register Cube API with authentication
await registerCubeRoutes(fastify, {
cubes: [employeesCube, departmentsCube],
drizzle: db,
schema: schema,
extractSecurityContext: async (request) => {
// request.user is populated by JWT verification
if (!request.user) {
throw new Error('Unauthorized')
}
return {
organisationId: request.user.organisationId,
userId: request.user.sub,
tenantId: request.user.tenantId
}
},
basePath: '/cubejs-api/v1' // Optional: custom prefix
})
// Protect routes
fastify.addHook('onRequest', fastify.authenticate)
await fastify.listen({ port: 3000 })
Hono Adapter
Installation
npm install hono drizzle-cube
Basic Setup
import { Hono } from 'hono'
import { createCubeApp } from 'drizzle-cube/adapters/hono'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const app = new Hono()
// Create Cube API
const cubeApp = createCubeApp({
cubes: [employeesCube, departmentsCube], // Array of cube definitions
drizzle: db,
schema: schema,
extractSecurityContext: async (c) => {
// Extract from Hono context
const authHeader = c.req.header('Authorization')
const token = authHeader?.replace('Bearer ', '')
// Decode token and extract context
const user = await verifyToken(token)
return {
organisationId: user.organisationId,
userId: user.id
}
}
})
// Mount the API
app.route('/cubejs-api/v1', cubeApp)
// Start server (for Node.js)
export default app
With JWT Middleware
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
import { createCubeApp } from 'drizzle-cube/adapters/hono'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const app = new Hono()
// JWT middleware
app.use('/cubejs-api/*', jwt({
secret: process.env.JWT_SECRET!
}))
const cubeApp = createCubeApp({
cubes: [employeesCube, departmentsCube],
drizzle: db,
schema: schema,
extractSecurityContext: async (c) => {
// Get JWT payload from context
const payload = c.get('jwtPayload')
return {
organisationId: payload.organisationId,
userId: payload.sub,
permissions: payload.permissions
}
}
})
app.route('/cubejs-api/v1', cubeApp)
export default app
Edge Runtime (Cloudflare Workers)
import { Hono } from 'hono'
import { createCubeApp } from 'drizzle-cube/adapters/hono'
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
const app = new Hono<{ Bindings: { DB: D1Database } }>()
const cubeApp = createCubeApp({
cubes: [employeesCube, departmentsCube],
drizzle: drizzle(c.env.DB), // D1 database binding
schema: schema,
extractSecurityContext: async (c) => {
const authHeader = c.req.header('Authorization')
// Extract and verify token
const user = await verifyEdgeToken(authHeader)
return {
organisationId: user.orgId,
userId: user.sub
}
}
})
app.route('/cubejs-api/v1', cubeApp)
export default app
Next.js Adapter
Installation
npm install next drizzle-cube
API Route Setup (App Router)
// app/api/cubejs/[...cube]/route.ts
import { NextRequest } from 'next/server'
import { createCubeHandlers } from 'drizzle-cube/adapters/nextjs'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from '@/lib/schema'
import { employeesCube, departmentsCube } from '@/lib/cubes'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
// Create all cube handlers
const handlers = createCubeHandlers({
cubes: [employeesCube, departmentsCube], // Array of cube definitions
drizzle: db,
schema: schema,
extractSecurityContext: async (request) => {
// Get session from Next Auth
const session = await getServerSession(authOptions)
if (!session?.user) {
throw new Error('Unauthorized')
}
return {
organisationId: session.user.organisationId,
userId: session.user.id
}
}
})
// POST handler for /load and /sql
export async function POST(
request: NextRequest,
context: { params: Promise<{ cube: string[] }> }
) {
const params = await context.params
const endpoint = params.cube[0]
// Route to appropriate handler
if (endpoint === 'load') {
return handlers.load(request, context)
} else if (endpoint === 'sql') {
return handlers.sql(request, context)
}
return new Response('Not Found', { status: 404 })
}
// GET handler for /meta
export async function GET(
request: NextRequest,
context: { params: Promise<{ cube: string[] }> }
) {
const params = await context.params
const endpoint = params.cube[0]
if (endpoint === 'meta') {
return handlers.meta(request, context)
}
return new Response('Not Found', { status: 404 })
}
Alternative: Individual Handler Creation
You can also create individual handlers:
// app/api/cubejs/load/route.ts
import { createLoadHandler } from 'drizzle-cube/adapters/nextjs'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from '@/lib/schema'
import { employeesCube, departmentsCube } from '@/lib/cubes'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const loadHandler = createLoadHandler({
cubes: [employeesCube, departmentsCube],
drizzle: db,
schema: schema,
extractSecurityContext: async (request) => {
// Your security context extraction logic
return {
organisationId: 'org-1',
userId: 'user-1'
}
}
})
export const POST = loadHandler
// app/api/cubejs/meta/route.ts
import { createMetaHandler } from 'drizzle-cube/adapters/nextjs'
// ... same imports and setup ...
const metaHandler = createMetaHandler({
cubes: [employeesCube, departmentsCube],
drizzle: db,
schema: schema,
extractSecurityContext: async (request) => {
return { organisationId: 'org-1', userId: 'user-1' }
}
})
export const GET = metaHandler
Next.js Adapter Options
interface NextAdapterOptions {
cubes: Cube[] // REQUIRED: Array of cube definitions
drizzle: DrizzleDatabase // REQUIRED: Drizzle database instance
schema?: any // RECOMMENDED: Database schema
extractSecurityContext: (request, context?) => SecurityContext | Promise<SecurityContext> // REQUIRED
engineType?: 'postgres' | 'mysql' | 'sqlite' // Optional: auto-detected
cors?: NextCorsOptions // Optional: CORS configuration
runtime?: 'edge' | 'nodejs' // Optional: Runtime environment
}
Security Context Patterns
Session-Based Authentication
extractSecurityContext: async (req) => {
const session = await getSession(req)
if (!session) {
throw new Error('Unauthorized: No session')
}
return {
organisationId: session.organisationId,
userId: session.userId,
role: session.role
}
}
JWT Token Authentication
import jwt from 'jsonwebtoken'
extractSecurityContext: async (req) => {
const authHeader = req.headers.authorization
const token = authHeader?.replace('Bearer ', '')
if (!token) {
throw new Error('Unauthorized: No token provided')
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as any
return {
organisationId: payload.orgId,
userId: payload.sub,
tenantId: payload.tenantId
}
} catch (error) {
throw new Error('Unauthorized: Invalid token')
}
}
API Key Authentication
extractSecurityContext: async (req) => {
const apiKey = req.headers['x-api-key']
if (!apiKey) {
throw new Error('Unauthorized: No API key')
}
// Lookup API key in database
const keyInfo = await db
.select()
.from(apiKeys)
.where(eq(apiKeys.key, apiKey))
.limit(1)
if (!keyInfo[0]) {
throw new Error('Unauthorized: Invalid API key')
}
return {
organisationId: keyInfo[0].organisationId,
userId: keyInfo[0].userId,
scope: keyInfo[0].scope
}
}
Multi-Tenant with Sub-domains
extractSecurityContext: async (req) => {
const host = req.headers.host
const subdomain = host?.split('.')[0]
// Lookup organization by subdomain
const org = await db
.select()
.from(organisations)
.where(eq(organisations.subdomain, subdomain))
.limit(1)
if (!org[0]) {
throw new Error('Invalid subdomain')
}
// Also validate user session
const session = await getSession(req)
return {
organisationId: org[0].id,
userId: session?.userId,
tenantId: org[0].tenantId
}
}
Available Endpoints
All adapters expose these Cube.js-compatible endpoints:
POST /cubejs-api/v1/load
Execute semantic queries and return results.
// Request
POST /cubejs-api/v1/load
Content-Type: application/json
Authorization: Bearer <token>
{
"measures": ["Employees.count"],
"dimensions": ["Departments.name"]
}
// Response
{
"data": [
{
"Departments.name": "Engineering",
"Employees.count": 50
}
],
"annotation": { ... },
"requestId": "req-123",
"slowQuery": false
}
POST /cubejs-api/v1/sql
Generate SQL without executing (dry-run).
// Request
POST /cubejs-api/v1/sql
Content-Type: application/json
Authorization: Bearer <token>
{
"measures": ["Employees.count"],
"dimensions": ["Departments.name"]
}
// Response
{
"sql": {
"sql": ["SELECT departments.name, COUNT(employees.id) FROM ..."],
"params": ["org-123"]
}
}
GET /cubejs-api/v1/meta
Get cube metadata (dimensions, measures, types).
// Request
GET /cubejs-api/v1/meta
Authorization: Bearer <token>
// Response
{
"cubes": [
{
"name": "Employees",
"title": "Employees",
"measures": [...],
"dimensions": [...]
}
]
}
Environment Configuration
# .env file
# Database connection
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
# JWT authentication
JWT_SECRET=your-secret-key
# API configuration
PORT=3000
# Multi-database support (optional - auto-detected)
DB_TYPE=postgres # or mysql, sqlite
Complete Example: Express with TypeScript
// src/server.ts
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import { createCubeRouter } from 'drizzle-cube/adapters/express'
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
import { employeesCube, departmentsCube } from './cubes'
import { authenticateJWT } from './middleware/auth'
const queryClient = postgres(process.env.DATABASE_URL!)
const db = drizzle(queryClient, { schema })
const app = express()
// Middleware
app.use(helmet())
app.use(cors())
app.use(express.json())
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok' })
})
// Cube API (protected)
const cubeRouter = createCubeRouter({
cubes: [employeesCube, departmentsCube],
drizzle: db,
schema: schema,
extractSecurityContext: async (req, res) => {
if (!req.user) {
throw new Error('Unauthorized')
}
return {
organisationId: req.user.organisationId,
userId: req.user.id,
role: req.user.role,
permissions: req.user.permissions
}
},
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',')
}
})
app.use('/cubejs-api/v1', authenticateJWT, cubeRouter)
// Error handling
app.use((err: any, req: any, res: any, next: any) => {
console.error(err.stack)
res.status(500).json({
error: err.message,
requestId: req.id
})
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`🚀 Drizzle Cube API running on port ${PORT}`)
})
Defining Cubes
Cubes are defined separately and imported into the adapter:
// lib/cubes/employees.ts
import { defineCube } from 'drizzle-cube'
import { eq } from 'drizzle-orm'
import { employees } from '../schema'
export const employeesCube = defineCube('Employees', {
// Security context filtering (MANDATORY)
sql: (ctx) => ({
from: employees,
where: eq(employees.organisationId, ctx.securityContext.organisationId)
}),
dimensions: {
id: {
type: 'number',
sql: () => employees.id,
primaryKey: true
},
name: {
type: 'string',
sql: () => employees.name
}
},
measures: {
count: {
type: 'count',
sql: () => employees.id
}
}
})
// lib/cubes/index.ts
export { employeesCube } from './employees'
export { departmentsCube } from './departments'
// ... export other cubes
Best Practices
- Always validate security context - Never trust client input
- Use HTTPS in production - Protect API traffic
- Implement rate limiting - Prevent abuse
- Log queries - Monitor performance and usage
- Handle errors gracefully - Return meaningful error messages
- Validate environment variables - Check configuration on startup
- Pass cubes as array - Let adapters create the semantic layer internally
- Provide schema for type safety - Enables better TypeScript inference
Common Pitfalls
- Missing authentication - Always protect Cube API endpoints
- Wrong function names - Use
createCubeRouterfor Express, notcreateCubeApi - Passing compiler instead of cubes - Adapters expect
cubesarray, NOT asemanticLayerparameter - Exposing internal errors - Sanitize error messages in production
- No security context validation - Verify context contains required fields
- Incorrect CORS configuration - Configure CORS for your client domains
- Missing database connection pooling - Use connection pools for production
Next Steps
- Define cubes with the
cube-definitionskill - Build queries with the
queriesskill - Create dashboards with the
dashboardskill - Configure charts with the chart-specific skills