LoopKit
Getting Started
Install and set up LoopKit 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 @loopkit/nestjsPeer dependencies (install as needed):
# Required for Prisma adapter
bun add @prisma/client
# Optional content transforms
bun add marked # Markdown rendering
bun add katex # Math rendering
bun add highlight.js # Code syntax highlighting
bun add sanitize-html # XSS protectionFrontend
bun add @loopkit/reactPeer dependencies:
bun add react react-dom # React 18 or 19Backend Setup
1. Register the Module
import { Module } from '@nestjs/common';
import { LoopKitModule, PrismaLoopKitAdapter } from '@loopkit/nestjs';
import { PrismaService } from './prisma.service';
@Module({
imports: [
LoopKitModule.register({
storage: new PrismaLoopKitAdapter(new PrismaService()),
}),
],
})
export class AppModule {}2. Create a Controller
import { Controller, Post, Body, Param } from '@nestjs/common';
import { ReviewSessionService, DeckService } from '@loopkit/nestjs';
@Controller('loopkit')
export class FlashcardController {
constructor(
private readonly session: ReviewSessionService,
private readonly decks: DeckService,
) {}
@Post('decks/:deckId/study')
async buildQueue(@Param('deckId') deckId: string) {
return this.session.buildQueue(deckId);
}
@Post('cards/:cardId/grade')
async gradeCard(
@Param('cardId') cardId: string,
@Body() body: { grade: string; timeTakenMs: number },
) {
return this.session.gradeCard(cardId, body.grade as any, body.timeTakenMs);
}
}3. Seed Default Note Types
On application startup, seed the default note types:
import { NoteTypeService } from '@loopkit/nestjs';
@Injectable()
export class AppInitService implements OnModuleInit {
constructor(private readonly noteTypes: NoteTypeService) {}
async onModuleInit() {
await this.noteTypes.seedDefaults();
}
}Frontend Setup
1. Wrap Your App with the Provider
import { LoopKitProvider } from '@loopkit/react';
import '@loopkit/react/styles.css';
function App() {
return (
<LoopKitProvider apiUrl="/api/loopkit">
<YourApp />
</LoopKitProvider>
);
}2. Use the Review Session Component
import { ReviewSession } from '@loopkit/react';
function StudyPage({ deckId }: { deckId: string }) {
return (
<ReviewSession
deckId={deckId}
onComplete={(summary) => {
console.log(`Reviewed ${summary.totalReviewed} cards`);
}}
/>
);
}3. Or Build a Custom UI with Hooks
import { useReviewSession } from '@loopkit/react';
function CustomStudy({ deckId }: { deckId: string }) {
const {
sessionState,
currentCard,
renderedContent,
nextIntervals,
progress,
startSession,
showAnswer,
grade,
} = useReviewSession();
if (sessionState === 'idle') {
return <button onClick={() => startSession(deckId)}>Start Study</button>;
}
if (sessionState === 'reviewing' && renderedContent) {
return (
<div>
<p>{progress.reviewed} / {progress.total}</p>
<div dangerouslySetInnerHTML={{ __html: renderedContent.front }} />
<button onClick={showAnswer}>Show Answer</button>
</div>
);
}
if (sessionState === 'answered' && renderedContent) {
return (
<div>
<div dangerouslySetInnerHTML={{ __html: renderedContent.back }} />
<div>
<button onClick={() => grade('again')}>Again {nextIntervals?.again}</button>
<button onClick={() => grade('hard')}>Hard {nextIntervals?.hard}</button>
<button onClick={() => grade('good')}>Good {nextIntervals?.good}</button>
<button onClick={() => grade('easy')}>Easy {nextIntervals?.easy}</button>
</div>
</div>
);
}
return <p>Session complete!</p>;
}Next Steps
- Learn about the Architecture and design principles
- Explore the Backend API reference
- Explore the Frontend API reference