Hey DEV Community 👋,
In my last post about building Nexus LMS (an enterprise-grade backend using NestJS 11 and Notion as a Headless CMS), I got a very fair piece of feedback from the comments: "You forgot to show the code and what it actually looks like."
They were 100% right. An architecture blueprint is just theory until you see the engine running.
So, today, let's open the hood. Here is the technical meat of how the Nexus LMS backend actually works, who it's built for, and how the data flows using TypeORM.
🎯 Who is this for?
I built this architecture specifically for:
- Indie Hackers & Tech Creators: Who want to host their own courses/digital products without paying Teachable or Kajabi $100+/month.
- SaaS Founders: Who need a bulletproof, scalable NestJS boilerplate with Auth, RBAC, and a well-structured TypeORM database already configured.
🏗️ The Database: TypeORM Entities
The core of the LMS is mapped out in PostgreSQL using TypeORM. Here is how we structure the entity for a Lesson, keeping a strict relational structure and a reference to the Notion Page ID for synchronization:
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { Module } from './module.entity';
@Entity('lessons')
export class Lesson {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
title: string;
@Column({ type: 'text', nullable: true })
content: string; // This gets populated from Notion
@Column({ unique: true })
notionPageId: string; // The hook to our Headless CMS
@ManyToOne(() => Module, (module) => module.lessons)
module: Module;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
🧠 The Engine: Syncing Notion to PostgreSQL
We don't want to build a clunky rich-text editor in React. Instead, we write our lessons in Notion. Our NestJS worker hits the Notion API, parses the blocks, and saves them to our database.
Here is a simplified snippet of that sync logic using the TypeORM Repository pattern:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Client } from '@notionhq/client';
import { Lesson } from './entities/lesson.entity';
@Injectable()
export class NotionSyncService {
private notion = new Client({ auth: process.env.NOTION_API_KEY });
constructor(
@InjectRepository(Lesson)
private lessonRepository: Repository<Lesson>
) {}
async syncLessonContent(pageId: string) {
// 1. Fetch blocks from Notion
const response = await this.notion.blocks.children.list({ block_id: pageId });
// 2. Parse the proprietary blocks into clean HTML/Markdown
const parsedContent = this.parseNotionBlocks(response.results);
// 3. Update the PostgreSQL database instantly
return this.lessonRepository.update(
{ notionPageId: pageId },
{ content: parsedContent }
);
}
}
📡 The Output: Clean API Responses
Security and clean data structure are priorities. When a frontend client (or game engine) requests the course tree via our Swagger UI, NestJS serializes the data perfectly (stripping out sensitive info like passwordHash using @Exclude() decorators).
Here is the actual JSON output the backend serves:
[
{
"id": "96efd541-2da7-4027-bfc6-8c48e6d5f350",
"title": "Nexus Enterprise Architecture",
"description": "The definitive guide to building scalable backend systems.",
"modules": [
{
"id": "141bac3c-c908-44ef-b36a-3ef155478add",
"title": "Module 4: Headless CMS Integration",
"lessons": [
{
"id": "4b0dfbaf-0b22-482a-b908-bab6e6bd6bb8",
"title": "Lesson 4.1: Syncing Notion Blocks to PostgreSQL",
"content": "Welcome to the core of the Nexus Blueprint...",
"notionPageId": "30458554-f35a-80c7-84f8-ea8059303abc"
}
]
}
]
}
]
🚀 Get the Full Codebase
Building this from scratch takes hundreds of hours of trial, error, and refactoring. If you want to skip the boilerplate and instantly spin up this architecture, I’ve packaged the entire ecosystem (NestJS backend, TypeORM entities, Docker setup, and the custom Telegram distribution bot).
You can grab the code or join the Waitlist here:
👉https://t.me/Nexus_sal_bot
Top comments (0)