RoomKitExamples
Full Stack Example
Complete room booking application with NestJS backend and Next.js frontend
Overview
This example combines the NestJS API and Next.js App into a full-stack room booking application with all major features enabled.
Project Structure
roomkit-app/
├── backend/
│ ├── src/
│ │ ├── app.module.ts # RoomKitModule registration
│ │ ├── booking.controller.ts # Booking CRUD + lifecycle
│ │ ├── room.controller.ts # Room management
│ │ ├── location.controller.ts # Location hierarchy
│ │ ├── admin.controller.ts # Bulk ops, blackouts, config
│ │ └── main.ts
│ ├── prisma/
│ │ └── schema.prisma # RoomKit models
│ └── package.json
├── frontend/
│ ├── app/
│ │ ├── layout.tsx # RoomKitProvider
│ │ ├── rooms/page.tsx # Availability search
│ │ ├── bookings/[id]/page.tsx # Booking detail
│ │ └── admin/page.tsx # Admin dashboard
│ └── package.json
└── docker-compose.yml # PostgreSQLBackend Setup
Module Registration
import { Module } from '@nestjs/common';
import { RoomKitModule, PrismaRoomKitAdapter } from '@hfu.digital/roomkit-nestjs';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
@Module({
imports: [
RoomKitModule.register({
storage: new PrismaRoomKitAdapter(prisma),
priorities: [
{ name: 'EXAM', weight: 200 },
{ name: 'LECTURE', weight: 100 },
{ name: 'SEMINAR', weight: 75 },
{ name: 'LAB', weight: 60 },
{ name: 'STUDY_GROUP', weight: 50 },
{ name: 'OPEN', weight: 25 },
],
features: {
exams: true,
bulkOperations: true,
},
travelTimeMatrix: {
'campus-furtwangen|campus-schwenningen': 25,
'campus-furtwangen|campus-tuttlingen': 40,
'campus-schwenningen|campus-tuttlingen': 20,
},
events: {
BookingRequested: (event) => {
// Send notification to room admin
},
BookingConfirmed: (event) => {
// Send confirmation email to requester
},
BlackoutImpactDetected: (event) => {
// Notify affected bookers
for (const booking of event.payload.impactedBookings) {
console.log(`Booking ${booking.id} impacted by blackout`);
}
},
BulkOperationCompleted: (event) => {
console.log(
`Bulk operation completed: ${event.payload.processedItems} items, ` +
`${event.payload.conflictsDetected} conflicts`,
);
},
},
}),
],
})
export class AppModule {}Admin Controller (Bulk Operations)
import { Controller, Post, Get, Body, Param, Query } from '@nestjs/common';
import {
BulkOperationService,
BlackoutService,
ConfigService,
TravelTimeService,
} from '@hfu.digital/roomkit-nestjs';
@Controller('admin')
export class AdminController {
constructor(
private readonly bulkOps: BulkOperationService,
private readonly blackouts: BlackoutService,
private readonly config: ConfigService,
private readonly travelTime: TravelTimeService,
) {}
@Post('semester-import')
async importSemester(@Body() body: {
entries: {
roomId: string;
startsAt: string;
endsAt: string;
requesterId: string;
purposeType: string;
title: string;
recurrence?: {
frequency: string;
daysOfWeek: number[];
calendarWeeks?: number[];
seriesStartsAt: string;
seriesEndsAt: string;
};
}[];
triggeredBy: string;
}) {
return this.bulkOps.semesterImport(
{
entries: body.entries.map((e) => ({
...e,
startsAt: new Date(e.startsAt),
endsAt: new Date(e.endsAt),
recurrence: e.recurrence
? {
...e.recurrence,
seriesStartsAt: new Date(e.recurrence.seriesStartsAt),
seriesEndsAt: new Date(e.recurrence.seriesEndsAt),
exceptionDates: [],
calendarWeeks: e.recurrence.calendarWeeks ?? null,
}
: undefined,
})),
},
body.triggeredBy,
);
}
@Post('blackouts')
async createBlackout(@Body() body: {
locationNodeId: string;
scope: string;
title: string;
reason?: string;
startsAt: string;
endsAt: string;
}) {
const blackout = await this.blackouts.create({
...body,
startsAt: new Date(body.startsAt),
endsAt: new Date(body.endsAt),
isRecurring: false,
recurrenceRuleId: null,
});
// Analyze impact on existing bookings
const impacted = await this.blackouts.analyzeImpact(blackout);
return { blackout, impactedBookings: impacted.length };
}
@Post('config')
async setConfig(@Body() body: {
locationNodeId: string | null;
key: string;
value: string;
}) {
return this.config.set(body.locationNodeId, body.key, body.value);
}
@Get('config/:locationNodeId')
async getEffectiveConfig(@Param('locationNodeId') locationNodeId: string) {
const config = await this.config.getEffectiveConfig(locationNodeId);
return Object.fromEntries(config);
}
@Get('travel-time')
async validateTravelTime(
@Query('personId') personId: string,
@Query('roomId') roomId: string,
@Query('startsAt') startsAt: string,
@Query('endsAt') endsAt: string,
) {
return this.travelTime.validatePersonSchedule(personId, {
roomId,
startsAt: new Date(startsAt),
endsAt: new Date(endsAt),
});
}
}Frontend Setup
Semester Import Dashboard
'use client';
import { useState } from 'react';
import { useBulkOperationStatus, BulkImportProgress } from '@hfu.digital/roomkit-react';
export default function AdminPage() {
const [operationId, setOperationId] = useState<string | null>(null);
const bulkStatus = useBulkOperationStatus({
operationId: operationId ?? '',
enabled: !!operationId,
pollIntervalMs: 1000,
onComplete: (op) => {
alert(`Import complete: ${op.processedItems} items, ${op.conflictsDetected} conflicts`);
},
});
const handleImport = async () => {
const response = await fetch('/api/roomkit/admin/semester-import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
entries: [
{
roomId: 'room-101',
startsAt: '2026-03-02T10:00:00Z',
endsAt: '2026-03-02T11:30:00Z',
requesterId: 'scheduler',
purposeType: 'LECTURE',
title: 'Software Engineering',
recurrence: {
frequency: 'weekly',
daysOfWeek: [1],
seriesStartsAt: '2026-03-02',
seriesEndsAt: '2026-07-13',
},
},
],
triggeredBy: 'admin-user',
}),
});
const data = await response.json();
setOperationId(data.id);
};
return (
<div style={{ padding: '2rem' }}>
<h1>Semester Import</h1>
<button onClick={handleImport}>Start Import</button>
{bulkStatus.data && (
<BulkImportProgress operation={bulkStatus.data} />
)}
</div>
);
}Exam Scheduling
'use client';
import { useExamSessions, ExamScheduleView } from '@hfu.digital/roomkit-react';
export default function ExamsPage() {
const { data, isLoading } = useExamSessions({
filters: {
startsAt: new Date('2026-07-01'),
endsAt: new Date('2026-07-31'),
},
pagination: { limit: 50 },
});
if (isLoading) return <p>Loading exam schedule...</p>;
return (
<div style={{ padding: '2rem' }}>
<h1>Exam Schedule — July 2026</h1>
{data && (
<ExamScheduleView
sessions={data.items}
onSessionClick={(session) => {
window.location.href = `/bookings/${session.bookingId}`;
}}
/>
)}
</div>
);
}Docker Compose
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: roomkit
POSTGRES_USER: roomkit
POSTGRES_PASSWORD: roomkit
ports:
- '5432:5432'
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Running the Full Stack
# Start the database
docker compose up -d
# Backend
cd backend
bun install
bunx prisma migrate dev
bun run start:dev
# Frontend (separate terminal)
cd frontend
bun install
bun run dev