Привет читатель!
В данной статье я раскрою один из секретов Полишинеля, точнее то как создать и редактировать .md файлы на бэке в Nest.js, да звучит банально, но для таких же свитчеров как я думаю будет полезно.
Но для начала немного отойдем от сабжа и обсудим зачем, если кратко то я сейчас разрабатываю пет проект криптотрекера, и под впечатлением от Obsidian решил добавить функцию заметок себе. Ну и конечно чтобы это все работало и у меня и в Obsidian. Плюс уже на фронте можно прикрутить красоты с помощью LaTeX + Math.js и Mermaid, что актуально для финансовых приложений типа моего.
Пример ниже сделан на стеке: npm Nest.js Prisma SQLite.
Дополнительно используются пакеты: uuid и gray-matter.
Для справки:
uuid - генератор id, использую чтобы заранее сгенерировать id до записи в бд.
gray-matter - парсер для YAML, использую в примере для записи метаданных в заметку.
Можно конечно обойтись и без них, но не всегда стоит придумывать велосипед.
В примерах ниже я опустил DTO чтобы не раздувать по попусту текст, если они вам нужны то их можно найти у меня в гите вместе со схемами для бд, ссылка в профиле.
Настройка:
gray-matter
npm install --save gray-matter import * as matter from 'gray-matter';
uuid
npm i uuid import { v4 as uuidv4 } from 'uuid';
Дефолтные node фичи
import * as fs from 'fs';//для создания файла и работы с файловой системой import * as path from 'path';//для работы с директориями
Код для всех функций из класса:
@Injectable() export class NotesService { private vaultDir = path.join(process.cwd(), 'vault', 'notes');// storage dir constructor(private prisma: PrismaService,) { if (!fs.existsSync(this.vaultDir)) { fs.mkdirSync(this.vaultDir, { recursive: true }); }// check and create dir / { recursive: true } - create folder chain } ... }
Здесь определен путь для хранения файлов и его создание.
Создание заметки:
async createNote(dto: CreateNoteDto){ const id = uuidv4();//nead for meta in obs and db const now = new Date().toISOString(); let tagNames: string[] = []; if (dto.marks?.length) { const tags = await this.prisma.marks.findMany({ where: { id: { in: dto.marks } }, select: { name: true }, }); tagNames = tags.map((t) => t.name); }// get tag names by id const frontmatter = { id, name: dto.name, linkedTo: dto.linkedTo, tags: tagNames, createdAt: now, updatedAt: now, }//gen meta data for obsidian const mdContent = matter.stringify(dto.content || '', frontmatter);//gen file with meta data const fileName = dto.name + '.md'; const filePath = path.join(this.vaultDir, fileName); fs.writeFileSync(filePath, mdContent, 'utf8');//save .md file const note = await this.prisma.notes.create({ data: { id, name: dto.name, userId: dto.userId, linkedTo: dto.linkedTo, createdAt: new Date(now), updatedAt: new Date(now), path: filePath, marks: JSON.stringify(dto.marks) }, });// save meta in db return note; }
В этом коде я заранее создаю id чтобы использовать его для записи в заметку, записываю полученные данные в файл который затем создается в указанной выше директории, при первом запуске директория создастся автоматически. Ну и в конце записываю данные в бд, для связывания с другими фичами приложения.
Тут есть важный момент, сам текст заметки передается в content строкой с переносами (/n) и имеет такой вид:
"content": "# BTC анализ\nПроверка уровня поддержки 60k.\n\n#btc #strategy\n\n```math\n2 * (3 + 4)\n```\n\n```mermaid\ngraph TD\nBTC-->ETH\nETH-->SOL\n```"
Здесь уже используется LaTeX + Math.js и Mermaid. Дальше в статье будет наглядный пример с результатом.
Редактирование заметки:
async updateNote(dto: UpdateNoteDto){ const note = await this.prisma.notes.findFirst({ where: { id: dto.id, userId: dto.userId } });//check db if (!note) throw new NotFoundException('Note not found'); const filePath = note.path; if (!fs.existsSync(filePath)) throw new NotFoundException('File not found'); let tagNames: string[] = []; if (dto.marks?.length) { const tags = await this.prisma.marks.findMany({ where: { id: { in: dto.marks } }, select: { name: true }, }); tagNames = tags.map((t) => t.name); }// get tag names by id const fileContent = fs.readFileSync(filePath, 'utf8');//read file const parsed = matter(fileContent); const updatedFrontmatter = { ...parsed.data, name: dto.name ?? parsed.data.name, tags: tagNames, updatedAt: new Date().toISOString(), };// update meta const newContent = dto.content ?? parsed.content; const updatedFile = matter.stringify(newContent, updatedFrontmatter);// update content and meta fs.writeFileSync(filePath, updatedFile, 'utf8');// save .md file const updatedNote = await this.prisma.notes.update({ where: { id: dto.id }, data: { name: updatedFrontmatter.name, updatedAt: new Date(), marks: JSON.stringify(dto.marks) }, });// update meta in db return updatedNote; }
Здесь представлен код для изменения заметки, в целом он работает по тому же принципу, только здесь нам нужно сначала прочесть файл а потом внести изменения, ну и конечно не забываем обновить данные в бд.
Важно помнить, что при изменении текста нужно передавать всю заметку целиком, точечного изменять данные в .md файлах мы не можем, вернее это сложно и большого смысла в этом нет.
Получение заметки:
async getNoteById(id: string) { const note = await this.prisma.notes.findFirst({ where: { id } }); if (!note) throw new NotFoundException('Note not found'); const fileContent = fs.readFileSync(note.path, 'utf8');//raed .md file const parsed = matter(fileContent);// note body return { note, parsed } }
Ответ
[ { "id": "08ffce73-4e68-4c90-8e09-c77722dc6c80", "userId": "08ffce73-4e68-4c90-8e09-c77722dc6c80", "name": "Test", "isArchived": false, "marks": [ "35198333-3ff5-46c7-8eef-3c97d6b7652f", "034caa3f-042b-4f36-a0dd-f1cd2f2e6c31" ], "content": "# BTC анализ\nПроверка уровня поддержки 60k.\n\n#btc #strategy\n\n```math\n2 * (3 + 4)\n```\n\n```mermaid\ngraph TD\nBTC-->ETH\nETH-->SOL\n```" } ]
Код на получение очевидно делится на 2 части, получение из бд и из файла.
Удаление заметки:
async deleteNote(id: string) { const note = await this.prisma.notes.findUnique({ where: { id } }); if (!note) throw new NotFoundException('Note not found'); const filePath = note.path; if (fs.existsSync(filePath)) { fs.unlinkSync(filePath);// delete .md file } await this.prisma.notes.delete({ where: { id } }); return { success: true, message: 'Note deleted' }; }
Код на удаление максимально простой, ищет файл и удаляет его, ну и чистит базу.
Конечно все эти примеры не без недостатков, но это та база которая работает и на которой можно дальше строить свое приложения.
Ну и давайте посмотри что мы имеем в итоге:

На скриншоте у нас созданная заметка открытая в Obsidian.
Сверху у нас созданные YAML свойства, которые продублированы в бд.
Здесь у нас 2 экземпляра тегов, верхние теги идут из приложения, а нижние переданы вручную для демонстрации.
В теории можно использовать оба вида тегов.
Диаграмма создана с помощью Mermaid.
Ниже таблица с данными из бд.
id | name | userId | createdAt | updatedAt | isArchived | linkedTo | path | marks |
b4094146-35d1-4d61-add9-a5695e32961f | Test | 08ffce73-4e68-4c90-8e09-c77722dc6c80 | 1760605735388 | 1760605801852 | 0 | 08ffce73-4e68-4c90-8e09-c77722dc6c80 | C:\Users\userName\Desktop\MoneyMovementMonitor\mmm-1\apps\backend\vault\notes\Test.md | ["35198333-3ff5-46c7-8eef-3c97d6b7652f","034caa3f-042b-4f36-a0dd-f1cd2f2e6c31"] |
В примере я уже использовал LaTeX (рендер формулы) и Mermaid (диаграмма), результат видно на скрине с Obsidian, они позволяют красиво оформить математические формулы и расчеты а также строить диаграммы с помощью markdown.
Подобным образом мы можем встроить заметки практически в любое приложение, будь то календарь, таск менеджер или криптотрекер. Дальше все зависит от вашей фантазии и здравого смысла.
Я же планирую в дальнейшем связать свои заметки с данными в бд, в частности с аналитикой, графиками, транзакциями.
Фронт часть я планирую описать когда перейду к разработке фронта, скорее всего там же будет блок с работой над ошибками и недочетами.
Надеюсь эта статья кому то будет полезна.
Полезные ссылки:
uuid — генератор ID
gray‑matter — для YAML stringify/parse frontmatter.
LaTeX — для отображения формул
Math.js — для математических расчетов
Mermaid — для диаграмм
