CourseKitExamples
NestJS REST API
Complete NestJS backend example with all CourseKit endpoints
Overview
This example shows a complete NestJS REST API using CourseKit with Prisma adapters, covering schedule queries, event management, conflict checking, and availability.
Module Setup
// app.module.ts
import { Module } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
import {
CourseKitModule,
PrismaTimetableEventAdapter,
PrismaRoomAdapter,
PrismaInstructorAdapter,
PrismaGroupAdapter,
PrismaAvailabilityAdapter,
PrismaAcademicPeriodAdapter,
PrismaCourseAdapter,
} from '@hfu.digital/coursekit-nestjs';
import { ScheduleController } from './schedule.controller';
import { EventController } from './event.controller';
import { ConflictController } from './conflict.controller';
import { NotificationService } from './notification.service';
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),
}),
],
controllers: [ScheduleController, EventController, ConflictController],
providers: [NotificationService],
})
export class AppModule {}Schedule Controller
// schedule.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { QueryService, AvailabilityService } from '@hfu.digital/coursekit-nestjs';
@Controller('coursekit')
export class ScheduleController {
constructor(
private readonly query: QueryService,
private readonly availability: AvailabilityService,
) {}
@Get('schedule')
async getSchedule(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('instructorIds') instructorIds?: string,
@Query('roomIds') roomIds?: string,
@Query('groupIds') groupIds?: string,
@Query('courseIds') courseIds?: string,
@Query('periodId') periodId?: string,
) {
return this.query.getSchedule({
dateRange: {
start: new Date(startDate),
end: new Date(endDate),
},
instructorIds: instructorIds?.split(','),
roomIds: roomIds?.split(','),
groupIds: groupIds?.split(','),
courseIds: courseIds?.split(','),
periodId,
});
}
@Get('availability')
async getAvailability(
@Query('entityType') entityType: 'instructor' | 'room',
@Query('entityId') entityId: string,
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
) {
const dateRange = {
start: new Date(startDate),
end: new Date(endDate),
};
const [isAvailable, freeSlots] = await Promise.all([
this.availability.isAvailable(entityType, entityId, dateRange.start, 60),
this.availability.findFreeSlots(entityType, entityId, dateRange, 30),
]);
return { slots: [], freeSlots, isAvailable: isAvailable.available };
}
@Get('free-slots')
async findFreeSlots(
@Query('startDate') startDate: string,
@Query('endDate') endDate: string,
@Query('durationMin') durationMin: string,
@Query('instructorIds') instructorIds?: string,
@Query('roomIds') roomIds?: string,
) {
const entityIds = [
...(instructorIds?.split(',').map(id => ({ type: 'instructor' as const, id })) ?? []),
...(roomIds?.split(',').map(id => ({ type: 'room' as const, id })) ?? []),
];
return this.query.findFreeSlots({
dateRange: { start: new Date(startDate), end: new Date(endDate) },
durationMin: parseInt(durationMin, 10),
entityIds,
});
}
}Event Controller
// event.controller.ts
import { Controller, Get, Post, Patch, Delete, Body, Param } from '@nestjs/common';
import {
TimetableEventStorage,
RecurrenceService,
type TimetableEvent,
} from '@hfu.digital/coursekit-nestjs';
@Controller('coursekit/events')
export class EventController {
constructor(
private readonly eventStorage: TimetableEventStorage,
private readonly recurrence: RecurrenceService,
) {}
@Post()
async createEvent(@Body() body: {
title: string;
startTime: string;
durationMin: number;
recurrenceRule?: string | null;
roomId?: string | null;
courseId?: string | null;
periodId?: string | null;
metadata?: Record<string, unknown> | null;
}) {
// Validate RRULE if provided
if (body.recurrenceRule) {
const error = this.recurrence.validateRule(body.recurrenceRule);
if (error) {
throw new Error(`Invalid RRULE: ${error}`);
}
}
return this.eventStorage.create({
title: body.title,
startTime: new Date(body.startTime),
durationMin: body.durationMin,
recurrenceRule: body.recurrenceRule ?? null,
roomId: body.roomId ?? null,
courseId: body.courseId ?? null,
periodId: body.periodId ?? null,
metadata: body.metadata ?? null,
});
}
@Patch(':id')
async updateEvent(@Param('id') id: string, @Body() body: Partial<TimetableEvent>) {
return this.eventStorage.update(id, body);
}
@Delete(':id')
async deleteEvent(@Param('id') id: string) {
await this.eventStorage.delete(id);
}
@Post(':id/exceptions')
async createException(
@Param('id') eventId: string,
@Body() body: {
originalDate: string;
type: 'cancelled' | 'modified' | 'added';
newStartTime?: string | null;
newDurationMin?: number | null;
newRoomId?: string | null;
metadata?: Record<string, unknown> | null;
},
) {
return this.eventStorage.createException({
eventId,
originalDate: new Date(body.originalDate),
type: body.type,
newStartTime: body.newStartTime ? new Date(body.newStartTime) : null,
newDurationMin: body.newDurationMin ?? null,
newRoomId: body.newRoomId ?? null,
metadata: body.metadata ?? null,
});
}
}Conflict Controller
// conflict.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { ConflictService } from '@hfu.digital/coursekit-nestjs';
@Controller('coursekit/conflicts')
export class ConflictController {
constructor(private readonly conflicts: ConflictService) {}
@Post('check')
async checkConflicts(@Body() body: {
event: {
title: string;
startTime: string;
durationMin: number;
recurrenceRule?: string | null;
roomId?: string | null;
courseId?: string | null;
periodId?: string | null;
metadata?: Record<string, unknown> | null;
};
dateRange: { start: string; end: string };
}) {
return this.conflicts.dryRun(
{
title: body.event.title,
startTime: new Date(body.event.startTime),
durationMin: body.event.durationMin,
recurrenceRule: body.event.recurrenceRule ?? null,
roomId: body.event.roomId ?? null,
courseId: body.event.courseId ?? null,
periodId: body.event.periodId ?? null,
metadata: body.event.metadata ?? null,
},
{
start: new Date(body.dateRange.start),
end: new Date(body.dateRange.end),
},
);
}
}Domain Event Listener
// notification.service.ts
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
DOMAIN_EVENTS,
type EventCreatedPayload,
type ConflictDetectedPayload,
} from '@hfu.digital/coursekit-nestjs';
@Injectable()
export class NotificationService {
@OnEvent(DOMAIN_EVENTS.EVENT_CREATED)
handleEventCreated(payload: EventCreatedPayload) {
console.log('New event scheduled:', payload.event.title);
}
@OnEvent(DOMAIN_EVENTS.CONFLICT_DETECTED)
handleConflict(payload: ConflictDetectedPayload) {
const errors = payload.result.conflicts.filter(c => c.severity === 'error');
if (errors.length > 0) {
console.warn(`${errors.length} blocking conflicts for event ${payload.triggeringEventId}`);
}
}
}