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-nestjsPeer dependencies:
bun add @nestjs/common @nestjs/core @nestjs/event-emitter rxjs class-validator class-transformerFrontend
bun add @hfu.digital/coursekit-reactPeer dependencies:
bun add react react-domPrisma 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
- Learn about the Architecture and design principles
- Explore the Backend API reference
- Explore the Frontend API reference
- See common RRULE Patterns for academic scheduling