My App
CourseKit

Getting Started

Install and set up CourseKit in your NestJS backend and React frontend

Prerequisites

  • Bun or Node.js 18+
  • A NestJS application (backend)
  • A React 18/19 application (frontend)

Installation

Backend

bun add @hfu.digital/coursekit-nestjs

Peer dependencies:

bun add @nestjs/common @nestjs/core @nestjs/event-emitter rxjs class-validator class-transformer

Frontend

bun add @hfu.digital/coursekit-react

Peer dependencies:

bun add react react-dom

Prisma Schema

CourseKit uses structural typing — it never imports @prisma/client. Add these models to your Prisma schema:

model TimetableEvent {
    id              String              @id @default(uuid())
    title           String
    startTime       DateTime
    durationMin     Int
    recurrenceRule  String?
    metadata        Json?
    courseId         String?
    roomId          String?
    periodId        String?
    version         Int                 @default(0)
    createdAt       DateTime            @default(now())
    updatedAt       DateTime            @updatedAt

    exceptions      EventException[]
    instructors     EventInstructor[]
    groups          EventGroup[]
    room            Room?               @relation(fields: [roomId], references: [id])
    course          Course?             @relation(fields: [courseId], references: [id])
    period          AcademicPeriod?     @relation(fields: [periodId], references: [id])
}

model EventException {
    id              String              @id @default(uuid())
    eventId         String
    originalDate    DateTime
    type            String              // 'cancelled' | 'modified' | 'added'
    newStartTime    DateTime?
    newDurationMin  Int?
    newRoomId       String?
    metadata        Json?

    event           TimetableEvent      @relation(fields: [eventId], references: [id], onDelete: Cascade)
}

model EventInstructor {
    id              String              @id @default(uuid())
    eventId         String
    instructorId    String
    role            String              @default("primary")

    event           TimetableEvent      @relation(fields: [eventId], references: [id], onDelete: Cascade)
    instructor      Instructor          @relation(fields: [instructorId], references: [id])
}

model EventGroup {
    id              String              @id @default(uuid())
    eventId         String
    groupId         String

    event           TimetableEvent      @relation(fields: [eventId], references: [id], onDelete: Cascade)
    group           Group               @relation(fields: [groupId], references: [id])
}

model Instructor {
    id              String              @id @default(uuid())
    name            String
    email           String?
    tags            Json?

    events          EventInstructor[]
    availability    Availability[]
}

model Room {
    id              String              @id @default(uuid())
    name            String
    building        String?
    campus          String?
    capacity        Int                 @default(0)
    tags            Json?

    events          TimetableEvent[]
    availability    Availability[]
}

model Group {
    id              String              @id @default(uuid())
    name            String
    type            String              // 'fixed' | 'enrollment'
    maxCapacity     Int?

    events          EventGroup[]
    students        StudentGroup[]
}

model Student {
    id              String              @id @default(uuid())
    name            String
    email           String?

    groups          StudentGroup[]
}

model StudentGroup {
    id              String              @id @default(uuid())
    studentId       String
    groupId         String

    student         Student             @relation(fields: [studentId], references: [id])
    group           Group               @relation(fields: [groupId], references: [id])
}

model Course {
    id              String              @id @default(uuid())
    name            String
    code            String?
    parentId        String?
    metadata        Json?

    events          TimetableEvent[]
    parent          Course?             @relation("CourseHierarchy", fields: [parentId], references: [id])
    children        Course[]            @relation("CourseHierarchy")
}

model Availability {
    id              String              @id @default(uuid())
    entityType      String              // 'instructor' | 'room'
    entityId        String
    dayOfWeek       Int?
    specificDate    DateTime?
    startTime       DateTime
    endTime         DateTime
    type            String              // 'available' | 'blocked' | 'preferred'
    hardness        String              // 'hard' | 'soft'
    priority        Int                 @default(0)
    recurrenceRule  String?
}

model AcademicPeriod {
    id              String              @id @default(uuid())
    name            String
    type            String              // 'semester' | 'holiday' | 'exam' | 'break'
    startDate       DateTime
    endDate         DateTime
    parentId        String?

    events          TimetableEvent[]
    parent          AcademicPeriod?     @relation("PeriodHierarchy", fields: [parentId], references: [id])
    children        AcademicPeriod[]    @relation("PeriodHierarchy")
}

model LocationDistance {
    id              String              @id @default(uuid())
    fromCampus      String
    toCampus        String
    travelMinutes   Int
}

Backend Setup

1. Register the Module

import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import {
    CourseKitModule,
    PrismaTimetableEventAdapter,
    PrismaRoomAdapter,
    PrismaInstructorAdapter,
    PrismaGroupAdapter,
    PrismaAvailabilityAdapter,
    PrismaAcademicPeriodAdapter,
    PrismaCourseAdapter,
    PrismaLocationDistanceAdapter,
} from '@hfu.digital/coursekit-nestjs';

const prisma = new PrismaClient();

@Module({
    imports: [
        CourseKitModule.register({
            eventStorage: new PrismaTimetableEventAdapter(
                prisma.timetableEvent,
                prisma.eventException,
                prisma.eventInstructor,
                prisma.eventGroup,
            ),
            roomStorage: new PrismaRoomAdapter(prisma.room),
            instructorStorage: new PrismaInstructorAdapter(prisma.instructor),
            groupStorage: new PrismaGroupAdapter(prisma.group, prisma.studentGroup),
            availabilityStorage: new PrismaAvailabilityAdapter(prisma.availability),
            periodStorage: new PrismaAcademicPeriodAdapter(prisma.academicPeriod),
            courseStorage: new PrismaCourseAdapter(prisma.course),
            locationDistanceStorage: new PrismaLocationDistanceAdapter(prisma.locationDistance),
        }),
    ],
})
export class AppModule {}

2. Create a Controller

import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
import { QueryService, ConflictService } from '@hfu.digital/coursekit-nestjs';

@Controller('coursekit')
export class TimetableController {
    constructor(
        private readonly query: QueryService,
        private readonly conflicts: ConflictService,
    ) {}

    @Get('schedule')
    async getSchedule(
        @Query('startDate') startDate: string,
        @Query('endDate') endDate: string,
        @Query('instructorIds') instructorIds?: string,
    ) {
        return this.query.getSchedule({
            dateRange: {
                start: new Date(startDate),
                end: new Date(endDate),
            },
            instructorIds: instructorIds?.split(','),
        });
    }
}

3. Listen for Domain Events

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { DOMAIN_EVENTS, type EventCreatedPayload } from '@hfu.digital/coursekit-nestjs';

@Injectable()
export class NotificationService {
    @OnEvent(DOMAIN_EVENTS.EVENT_CREATED)
    handleEventCreated(payload: EventCreatedPayload) {
        console.log('New event:', payload.event.title);
    }
}

Frontend Setup

1. Wrap Your App with the Provider

import { CourseKitProvider } from '@hfu.digital/coursekit-react';

function App() {
    return (
        <CourseKitProvider apiUrl="/api/coursekit">
            <MySchedule />
        </CourseKitProvider>
    );
}

2. Display a Timetable

import { useTimetable, TimetableGrid, EventCard } from '@hfu.digital/coursekit-react';

function MySchedule() {
    const { data, loading } = useTimetable({
        dateRange: { start: '2026-03-02', end: '2026-03-08' },
        instructorIds: ['instructor-1'],
    });

    if (loading) return <div>Loading...</div>;

    return (
        <TimetableGrid weekStart="2026-03-02">
            {data.map(occ => (
                <EventCard
                    key={`${occ.eventId}-${occ.occurrenceDate}`}
                    title={occ.originalEvent.title}
                    startTime={occ.startTime}
                    durationMin={occ.durationMin}
                    isException={occ.isException}
                    exceptionType={occ.exceptionType}
                />
            ))}
        </TimetableGrid>
    );
}

Next Steps

On this page