My App
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());

On this page