HFU Digital Docs
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              # PostgreSQL

Backend Setup

Module Registration

backend/src/app.module.ts
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)

backend/src/admin.controller.ts
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

frontend/app/admin/page.tsx
'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

frontend/app/exams/page.tsx
'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

docker-compose.yml
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

On this page