A File-by-File Walkthrough of a Next.js + Redis Markdown Note App
Reading a Next.js Full-Stack Note Application from Scratch
This article is based on a next-blog note project and explains its technical background, directory structure, route design, component breakdown, and data services file by file.
1. Technical Background: Why Next.js
The README starts by explaining why this project chose Next.js and the key concepts behind it.
1.1 The Role of npx
npx is a tool built into npm that can run Node packages directly without needing to install dependencies globally.
npx = npm i -g create-next-app + create-next-app
npxis a tool built into npm that can run Node packages directly, without needing to install dependencies globally.- Using
npx create-next-appquickly scaffolds a project, which is equivalent to "first installingcreate-next-appglobally, then running it." - The benefit is convenience, suitable for quickly trying things out or testing whether a machine can run the project.
1.2 Why Next.js (React Full-Stack Scaffold)
Next.js is a React full-stack development scaffold. The README lists several key concepts:
| Concept | Meaning |
|---|---|
| SSR | Server-Side Rendering |
| SEO | Search Engine Optimization |
| RSC | React Server Component |
- SSR allows pages to be rendered on the server, which is good for SEO.
use clientis used to mark client components, working with the hydration mechanism.
These concepts will appear repeatedly in the code later — for example, whether a component includes "use client" determines if it is a server component or a client component.
2. Project Requirements: A Markdown Note System
The README clearly states what this project aims to do:
- Build a note system that supports CRUD (Create, Read, Update, Delete) for notes and supports markdown format.
- Key design: The database stores markdown text, but the page displays HTML, with the
markedlibrary handling the conversion in between.
Specific functional points broken down as follows:
- Two-column interface: Note list on the left, note content on the right (corresponding to
page.jsat/). - Click new to add a note, after which the left list updates synchronously.
- Edit functionality, allowing deletion of a note, with the left list updating synchronously.
- Edit the current note, supporting markdown.
- Search functionality.
The routes also correspond to these requirements (App Router file-based routing):
/add POST create
/note/[id] Dynamic route, page.js is the note detail
/note/[id]/edit Edit
/edit page for creating a new one
3. Directory Structure
The project uses Next.js's convention-based directory structure:
- app Main page directory
page.js Home page
layout.js Layout
[id] Dynamic route
- components Components
- lib Database operations, utility functions
- public Static resources (static server)
Core convention: Data business logic goes in the lib directory, components in components, and pages in app.
4. Configuring alias: @ Points to Root
In app/notes/[id]/page.js, when importing lib/redis.js, using a relative path would require writing ../../../lib/redis.js, which is cumbersome. So an alias shortcut was configured:
baseUrl: .
path:
@/components/*
@/lib/*
After configuration, @ directly points to the root directory, allowing you to write:
import Sidebar from '@/components/Sidebar'
import { getAllNotes } from '@/lib/redis'
This is already used in the actual code (app/layout.js, components/Sidebar.js, SidebarNoteItem.js).
5. Layout: app/layout.js
layout.js is the root layout of the page, and the code itself is an async component:
import './style.css'
import Sidebar from '@/components/Sidebar'
export default async function RootLayout({ children }) {
return (
<html>
<head>
<title>HHGZ's Blog</title>
<meta name="description" content="..." />
<meta name="keywords" content="llm,claude,deepseek,rag,langchain" />
</head>
<body>
<div className="container">
<div className="main">
<Sidebar/>
<section className="col note-viewer">{children}</section>
</div>
</div>
</body>
</html>
)
}
From this file, several knowledge points can be observed:
Layout hierarchy (organized in the README):
layout └── html ├── head │ ├── title │ └── meta └── body ├── nav Sidebar, navigation bar └── children page.jsTwo-column layout: Left side
<Sidebar/>(note list), right side<section className="col note-viewer">{children}</section>(note content), exactly matching the requirement for "left and right columns."childrenis the child page: The{children}position will render the correspondingpage.js.
6. Home Page Placeholder: app/page.js
// RSC component async asynchronous to await fetching backend data first
export default async function Page() {
return (
<div className="note--empty-state">
<span className="note-text--empty-state">
Click a note on the left to view something.
</span>
</div>
)
}
The comment highlights a key point: RSC components can be written as async because they need to await fetching data from the backend first. This home page hasn't written the data fetching logic yet, just returning an empty state prompt.
7. Component Breakdown: Specification-Driven Programming + BEF
7.1 Specification-Driven Programming
The README emphasizes an important working method — don't rush to write code before development, but first:
- Analyze requirements
- Determine the technical solution (Next.js)
- Break down task details: routes + components
- Plan which components are needed
Among these, "components are work units, AI-generated work units" — break large tasks into small components, then have AI generate them one by one.
The planned component tree for the project:
Sidebar
SidebarSearchField
EditButton (reusable)
SidebarNoteList
NoteItem
Note
NoteEditor Edit
NotePreview Responsible for the note preview interface
7.2 BEF Naming Convention
Paired with the atomic CSS framework Tailwind CSS, the project adopts the BEF naming convention (Block / Element / Modifier):
- Block: Block
- Element: Element, connected with
_ - Modifier: Modifier, connected with
__
Corresponding to class naming in the code, for example note--empty-state, note-text--empty-state, all follow this convention for easier maintenance.
7.3 The "to be continue" Comment Technique
The README mentions writing comments in the code to plan future tasks (to be continue), which is beneficial for team collaboration, memory, and maintenance — writing "things to do" in comments. Such placeholder comments can be seen in Sidebar.js:
<section className="sidebar-menu" role="menubar">
{/*SidebarMenu*/}
</section>
And comments explaining block semantics:
{/* sidebar
Block: e-commerce site, product introduction, reviews, images, price...
Semantically, it is an independent content area, a slideshow */}
8. Sidebar Component File-by-File Breakdown
8.1 Sidebar.js — Sidebar Container
export default async function Sidebar() {
const notes = await getAllNotes();
return (
<section className="col sidebar">
<Link href="/" className="sidebar-header">
<img className="logo" src="/logo.svg" width="22px" height="20px" role="presentation" />
<strong>LLM Notes</strong>
</Link>
<section className="sidebar-menu" role="menubar">
{/*SidebarMenu*/}
</section>
<nav>
<SidebarNoteList notes={notes} />
</nav>
</section>
)
}
Knowledge points:
Sidebaris also an async component, first usingawait getAllNotes()to fetch note data from the backend (Redis).- Uses
next/link's<Link>for internal navigation,href="/"goes back to the home page. - The image
role="presentation"indicates a purely decorative image, not exposing semantics to screen readers. - Inside
<nav>,<SidebarNoteList notes={notes} />is rendered, passing the fetched notes to the child component.
8.2 SidebarNoteList.js — Note List
export default async function SidebarNoteList({ notes }) {
const arr = Object.entries(notes); // Convert hash to 2D array for easy component mapping
if (arr.length == 0) {
return <div className="notes-empty">No Notes created yet!</div>
}
return (
<ul className="notes-list">
{
arr.map(([noteId, note]) => (
<li key={noteId}>
<SidebarNoteItem noteId={noteId} note={JSON.parse(note)} />
</li>
))
}
</ul>
)
}
Key points:
Object.entries(notes)converts the hash object into a 2D array[[key, value], ...], making it easy to iterate withmapto generate components.- When the array is empty, it renders the "No Notes created yet!" empty state.
- The value of each note is a JSON string retrieved from Redis, so
JSON.parse(note)is needed to restore it to an object before passing it to the child component. - Uses
noteIdas thekey.
8.3 SidebarNoteItem.js — Single Note
export default function SidebarNoteItem({ noteId, note }) {
const { title, content = '', updateTime } = note;
return (
<SidebarNoteItemContent
id={noteId}
title={note.title}
expandChildren={
<p className="sidebar-note-excerpt">
{content.substring(0, 20) || <i>(No content)</i>}
</p>
}
>
<header className="sidebar-note-header">
<strong>{title}</strong>
<small>{dayjs(updateTime).format('YYYY-MM-DD')}</small>
</header>
</SidebarNoteItemContent>
)
}
Knowledge points:
- Destructures note fields:
title,content(default empty string''),updateTime. - Excerpt: Uses
content.substring(0, 20)to take the first 20 characters, displaying<i>(No content)</i>if there is no content. - Time formatting: Uses
dayjs(updateTime).format('YYYY-MM-DD')(dayjs is an imported date library). - This reflects the idea of component reuse/extraction: splitting the list item into two layers,
SidebarNoteItem+SidebarNoteItemContent.
8.4 SidebarNoteItemContent.js — Client Component
"use client";
import { useState, useEffect } from 'react';
export default function SidebarNoteItemContent({ id, title, children, expandChildren }) {
return (
<>
{children}
</>
)
}
Knowledge points:
- The top-level
"use client"marks this as a client component (distinct from the default server components above). - Imports two React Hooks,
useStateanduseEffect(although they are not yet used in the current code). - The component receives four props:
id,title,children,expandChildren. The current implementation only renders{children}, andexpandChildren(the expanded excerpt) is not yet rendered — this is a placeholder design "leaving room for expansion," echoing the README's "to be continue comment technique."
8.5 SidebarNoteList2.js — Comparison: Inline Approach
The project also has a SidebarNoteList2.js, which does not extract a single note into a component but renders inline directly within map:
arr.map(([noteId, note]) => {
const { title, updateTime } = JSON.parse(note);
return (
<li key={noteId}>
<header className="sidebar-note-header">
<strong>{title}</strong>
<small>{dayjs(updateTime).format('YYYY-MM-DD HH:mm:ss')}</small>
</header>
</li>
)
})
Comparing with SidebarNoteList.js, the differences between the two precisely illustrate the distinction between "component extraction vs inline":
| SidebarNoteList.js | SidebarNoteList2.js | |
|---|---|---|
| Single item structure | Extracted into SidebarNoteItem component |
Directly inline within map |
| Excerpt field | Has content excerpt |
No excerpt, only title |
| Time format | YYYY-MM-DD |
YYYY-MM-DD HH:mm:ss |
The version with extracted components is more conducive to reuse and future expansion, while the inline version is simpler and more direct.
9. Data Service: lib/redis.js (Redis)
9.1 Why Redis
The README introduces that this project's data service chose Redis:
- Redis is a NOSQL in-memory database, defaulting to port 6379.
- No data tables, not a relational database, no SQL driver needed, data is stored in memory.
- Usage is "a bit like localStorage," directly using
key: value.
Its "advanced aspect" lies in: optimized storage methods and corresponding methods for different types of values:
| Data Type | Methods |
|---|---|
| String | get / set |
| Hash | hget / hset |
Typical uses: caching, counters, leaderboards.
9.2 Redis + MySQL Caching Scenario
The README gives a very intuitive example using "Juejin homepage article list":
- The homepage article list "remains unchanged for several minutes."
- When the first user visits, the MySQL database is queried to get the
postslist, which is stored in Redis inkey: valueformat. - When the next user comes, it reads directly from Redis, no longer querying MySQL.
This solves the "I/O bottleneck of readable/writable data," using Redis as a cache in front of MySQL.
9.3 lib/redis.js Code
// Node Redis client, driver
import Redis from 'ioredis';
const redis = new Redis(); // Default NOSQL
// Hash key string ID, value is serialized string of note
const initialData = {
"1702459181837": '{"title":"sunt aut",...}',
"1702459182837": '{"title":"qui est",...}',
"1702459188837": '{"title":"ea molestias",...}'
}
export async function getAllNotes() {
// Hash data type
const data = await redis.hgetall('notes');
if (Object.keys(data).length == 0) {
await redis.hmset("notes", initialData);
}
return await redis.hgetall('notes');
}
Knowledge points:
- Uses
ioredisas the Node Redis client (driver). new Redis()connects to a default configured Redis instance.- Data is stored using the hash type: the outer key is
notes, the inner field is a "string ID", and the value is a "serialized string (JSON) of the note." initialDatais three seed notes for initialization.getAllNoteslogic: firsthgetall('notes')fetches all notes; if empty,hmset("notes", initialData)writes the initial data; finally returns all notes.
This function is exactly the data source called by await getAllNotes() in Sidebar.js earlier, also confirming that "the lib directory holds Next.js data business logic."
9.4 About Interfaces: RPC
The end of the README also mentions /app/api/route.js — RPC remote calls for interfaces (this file has not been implemented in the current project, belonging to the part to be supplemented).
10. Summary
Project Summary
This is a markdown note application based on Next.js + Redis, with a clear overall architecture:
- Frontend display: Next.js's App Router, using "file-based routing,"
layout.jsresponsible for the left-right two-column layout,page.jsresponsible for each page. - Component system: Using "specification-driven programming" to plan first then implement, breaking down according to the BEF naming convention into a component tree of
Sidebar→SidebarNoteList→SidebarNoteItem→SidebarNoteItemContent; distinguishing client/server components via"use client". - Data layer: Using
ioredisto connect to Redis (NOSQL in-memory database), using the hash type to store notes (field as ID, value as JSON string),lib/redis.jsuniformly encapsulates data access. - Path optimization: Using alias to make
@/point directly to the root directory, avoiding lengthy relative paths.
Knowledge Point Summary
- npx: A tool built into npm, can run Node packages without global installation,
npx create-next-appfor quick scaffolding. - Next.js Core Concepts: SSR (Server-Side Rendering), SEO, RSC (React Server Components),
use clientclient components, hydration. - App Router File-Based Routing:
/addPOST,/note/[id]dynamic route,/edit, etc. - async Components: RSC components written as
asynccanawaitfetching data from the backend first. - Specification-Driven Programming: Analyze requirements and break down tasks (routes + components) before development; components are work units.
- BEF Naming Convention: Block / Element (
_) / Modifier (__), paired with Tailwind CSS atomic classes. Object.entries: Converts a hash object into a 2D array, convenient formaprendering lists.- JSON.parse: Redis stores strings, which must be parsed into objects when retrieved.
- dayjs: Date formatting library,
format('YYYY-MM-DD'), etc. - ioredis + Redis: NOSQL in-memory database (port 6379), hash type uses
hgetall/hmset, can be used for caching, counters, leaderboards, commonly used in Redis + MySQL caching scenarios. - alias Configuration:
@/components/*,@/lib/*,@points directly to the root directory. - "to be continue" Comment Technique: Using comments to plan pending tasks, beneficial for collaboration and maintenance.
This project may be small, but it has all the essential parts: from scaffolding, routing, and component breakdown to data services and cache design, it forms a complete learning path for Next.js full-stack development.