LoopKitExamples
NestJS REST API
Complete NestJS backend example with all LoopKit endpoints
Overview
This example shows a complete NestJS REST API using LoopKit with the Prisma adapter, including deck management, card creation, study sessions, and statistics.
Module Setup
// app.module.ts
import { Module, OnModuleInit } from '@nestjs/common';
import {
LoopKitModule,
PrismaLoopKitAdapter,
NoteTypeService,
createContentPipeline,
createMarkdownTransform,
createSanitizeTransform,
} from '@loopkit/nestjs';
import { parse } from 'marked';
import sanitize from 'sanitize-html';
import { PrismaService } from './prisma.service';
import { DeckController } from './deck.controller';
import { StudyController } from './study.controller';
import { CardController } from './card.controller';
@Module({
imports: [
LoopKitModule.register({
storage: new PrismaLoopKitAdapter(new PrismaService()),
contentPipeline: createContentPipeline([
createMarkdownTransform({ parse }),
createSanitizeTransform(sanitize),
]),
}),
],
controllers: [DeckController, StudyController, CardController],
})
export class AppModule implements OnModuleInit {
constructor(private readonly noteTypes: NoteTypeService) {}
async onModuleInit() {
await this.noteTypes.seedDefaults();
}
}Deck Controller
// deck.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
import {
DeckService,
ImportExportService,
type CreateDeckInput,
type UpdateDeckInput,
} from '@loopkit/nestjs';
@Controller('decks')
export class DeckController {
constructor(
private readonly decks: DeckService,
private readonly importExport: ImportExportService,
) {}
@Get()
async listDecks() {
return this.decks.getDeckTree();
}
@Get(':id')
async getDeck(@Param('id') id: string) {
return this.decks.getDeck(id);
}
@Post()
async createDeck(@Body() input: CreateDeckInput) {
return this.decks.createDeck(input);
}
@Put(':id')
async updateDeck(@Param('id') id: string, @Body() input: UpdateDeckInput) {
return this.decks.updateDeck(id, input);
}
@Delete(':id')
async deleteDeck(
@Param('id') id: string,
@Query('deleteCards') deleteCards: string,
) {
await this.decks.deleteDeck(id, deleteCards === 'true');
}
@Get(':id/config')
async getEffectiveConfig(@Param('id') id: string) {
return this.decks.getEffectiveConfig(id);
}
@Get(':id/export/json')
async exportJSON(
@Param('id') id: string,
@Query('includeReviewLogs') includeReviewLogs: string,
) {
return this.importExport.exportJSON(id, includeReviewLogs === 'true');
}
@Get(':id/export/csv')
async exportCSV(@Param('id') id: string) {
return this.importExport.exportCSV(id);
}
}Study Controller
// study.controller.ts
import { Controller, Post, Body, Param } from '@nestjs/common';
import {
ReviewSessionService,
type Grade,
} from '@loopkit/nestjs';
@Controller('study')
export class StudyController {
constructor(private readonly session: ReviewSessionService) {}
@Post('decks/:deckId/queue')
async buildQueue(
@Param('deckId') deckId: string,
@Body() options?: { newCardsLimit?: number; reviewCardsLimit?: number },
) {
return this.session.buildQueue(deckId, options);
}
@Post('cards/:cardId/grade')
async gradeCard(
@Param('cardId') cardId: string,
@Body() body: { grade: Grade; timeTakenMs: number },
) {
return this.session.gradeCard(cardId, body.grade, body.timeTakenMs);
}
@Post('reviews/:reviewLogId/undo')
async undoReview(@Param('reviewLogId') reviewLogId: string) {
await this.session.undoLastReview(reviewLogId);
}
}Card Controller
// card.controller.ts
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import {
NoteService,
NoteTypeService,
type CreateNoteInput,
type UpdateNoteInput,
} from '@loopkit/nestjs';
@Controller('cards')
export class CardController {
constructor(
private readonly notes: NoteService,
private readonly noteTypes: NoteTypeService,
) {}
@Get('note-types')
async listNoteTypes() {
return this.noteTypes.findNoteTypes();
}
@Post('notes')
async createNote(@Body() body: CreateNoteInput & { deckId: string }) {
const { deckId, ...input } = body;
return this.notes.createNote(input, deckId);
}
@Put('notes/:id')
async updateNote(@Param('id') id: string, @Body() input: UpdateNoteInput) {
return this.notes.updateNote(id, input);
}
@Delete('notes/:id')
async deleteNote(@Param('id') id: string) {
await this.notes.deleteNote(id);
}
}Error Handling
// loopkit-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import {
LoopKitError,
EntityNotFoundError,
ValidationError,
InvalidGradeError,
DuplicateEntityError,
} from '@loopkit/nestjs';
@Catch(LoopKitError)
export class LoopKitExceptionFilter implements ExceptionFilter {
catch(exception: LoopKitError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
if (exception instanceof EntityNotFoundError) {
status = HttpStatus.NOT_FOUND;
} else if (exception instanceof ValidationError || exception instanceof InvalidGradeError) {
status = HttpStatus.BAD_REQUEST;
} else if (exception instanceof DuplicateEntityError) {
status = HttpStatus.CONFLICT;
}
response.status(status).send({
statusCode: status,
error: exception.code,
message: exception.message,
});
}
}Register the filter globally in main.ts:
app.useGlobalFilters(new LoopKitExceptionFilter());