Design and Implementation of the Service Backend Architecture for Editor, Component Platform, and C‑End Application
This article details the overall backend architecture, module relationships, RESTful API design, database schemas, authentication flow, activity creation process, JSON schema validation, component platform APIs, and server‑side rendering considerations for a Node.js‑based service supporting editors, components, and end‑user pages.
This article is the first signed piece for the Rare Earth Juejin technical community and may not be reproduced within 14 days without permission.
In the previous article I explained the rationale behind our Node.js framework, database, authentication, and testing choices; this article dives into the overall server‑side architecture and the relationships between its modules.
The overall architecture diagram from the first column is reused here to illustrate the system.
Editor Backend Design
The editor module is the core operation area, handling user registration/login, activity creation/modification/preview/publishing, and media uploads.
It is divided into three parts: User Information, Activity Management, and Utility Tools.
The code follows a standard Restful API style: a route receives the request, a middleware validates the parameters, a controller dispatches the call, and a service interacts with the database via an ORM framework.
User‑related endpoints
POST /users/login (shared with registration)
GET /users/getUserInfo
PATCH /users/updateUserInfo
Activity‑related endpoints
POST /activity/
POST /activity/copy/:id
GET /activity/:id
PATCH /activity/:id
DELETE /activity/:id
POST /activity/publish/:id
GET /activity/list
Utility endpoints
POST /utils/uploadImg
POST /utils/uploadVideo
Below are the database table designs.
Data Table Design
Designing tables early is crucial for large projects to define field types, meanings, and relationships.
We use MySQL (relational) for user and activity metadata and MongoDB (document) for activity content JSON.
User Table
The user table stores username, password, phone number, gender, and avatar.
Column
Type
Comment
username
varchar
Unique username
password
varchar
Password
phoneNumber
varchar
Phone number
gender
int
1 male, 2 female, 0 secret
avatar
varchar
User avatar URL
const seq = require("../db/seq/seq");
const { STRING, DATE, BOOLEAN } = require("../db/seq/types");
const User = seq.define("user", {
username: { type: STRING, allowNull: false, unique: "username", comment: "用户名" },
password: { type: STRING, allowNull: false, comment: "密码" },
phoneNumber: { type: STRING, allowNull: false, unique: "phoneNumber", comment: "手机号" },
gender: { type: STRING, allowNull: false, defaultValue: 0, comment: "性别(1 男性,2 女性,0 保密)" },
avatar: { type: STRING, comment: "头像(图片地址)" }
});
module.exports = User;Activity Table
The activity table stores title, description, a reference to the MongoDB content document, author, cover image, status, and the latest publish timestamp.
Column
Type
Comment
title
varchar
Title
desc
varchar
Description
contentId
varchar
Reference to MongoDB content
author
varchar
Author username (foreign key to user)
coverImg
varchar
Cover image URL
status
int
0 deleted, 1 unpublished, 2 published, 3 forced offline
latestPublishAt
date
Last publish time
const seq = require("../db/seq/seq");
const { STRING, DATE, BOOLEAN, INTEGER } = require("../db/seq/types");
const UserModel = require("./UserModel");
const Activity = seq.define("activity", {
uuid: { type: STRING, allowNull: false, unique: "uuid", comment: "uuid" },
title: { type: STRING, allowNull: false, comment: "标题" },
desc: { type: STRING, comment: "描述" },
contentId: { type: STRING, allowNull: false, unique: "contentId", comment: "内容 id,内容存储在 mongodb 中" },
author: { type: STRING, allowNull: false, comment: "作者 username" },
coverImg: { type: STRING, comment: "封面图片 url" },
status: { type: STRING, allowNull: false, defaultValue: 1, comment: "状态:0-删除,1-未发布,2-发布,3-强制下线" },
latestPublishAt: { type: DATE, defaultValue: null, comment: "最后一次发布的时间" }
});
Activity.belongsTo(UserModel, { foreignKey: "author", targetKey: "username" });
module.exports = Activity;The author field in the activity table links to the username field in the user table, enabling queries for all activities created by a specific user.
Login and User Information
Login uses SMS verification via Tencent Cloud SMS. A 2‑minute cache prevents repeated sends; if the SMS fails to send, the cache is not set.
When a verification code is received, the server checks the cached code against the user input. If they match, the system looks up the user; existing users have their login time updated and receive a JWT token generated with jwt.sign . New users are created on‑the‑fly and also receive a JWT token. The token (prefixed with Bearer ) is placed in ctx.header.authorization and later verified with jwt.verify .
Note: When generating the token, a Bearer prefix is added; verification must strip this prefix using req.headers.authorization.split(" ")[1] to avoid JsonWebTokenError: invalid token .
Create Activity
The activity creation flow starts from a route , passes through a middleware for login checking, then a validator for input validation, and finally a controller that calls a service to interact with the databases.
/**
* @description Activity related routes
*/
const router = require("koa-router")();
const loginCheck = require("../middlewares/loginCheck");
const validator = require("../middlewares/validator");
const { activityInfoSchema } = require("../validator/activity");
router.prefix("/activity");
router.post("/", loginCheck, validator(activityInfoSchema), async (ctx) => {
const { username } = ctx.userInfo;
const { title, desc, content = {} } = ctx.request.body;
const res = await createActivity(username, { title, desc }, content);
ctx.body = res;
});
module.exports = router;The loginCheck middleware verifies the JWT token and populates ctx.userInfo . If verification fails, a predefined error response is returned.
module.exports = async function loginCheck(ctx, next) {
const errRes = new ErrorRes(loginCheckFailInfo);
const token = ctx.header.authorization;
if (!token) { ctx.body = errRes; return; }
let flag = true;
try {
const userInfo = await jwtVerify(token);
delete userInfo.password;
ctx.userInfo = userInfo;
} catch (error) {
flag = false;
ctx.body = errRes;
}
if (flag) await next();
};Input validation uses JSON Schema with the ajv library. The activity schema requires a title and defines optional fields such as desc , coverImg , contentId , and a nested content object.
const activityInfoSchema = {
type: "object",
required: ["title"],
properties: {
title: { type: "string", maxLength: 255 },
desc: { type: "string", maxLength: 255 },
coverImg: { type: "string", maxLength: 255 },
contentId: { type: "string", maxLength: 255 },
content: {
type: "object",
properties: {
_id: { type: "string", maxLength: 255 },
components: { type: "array" },
props: { type: "object" },
setting: { type: "object" }
}
}
}
};The controller createActivity checks the title, generates a short UUID, and calls the service.
async function createActivity(author, data = {}, content = {}) {
const { title } = data;
if (!title) return new ErrorRes(createActivityFailInfo, "标题不能为空");
const uuid = uuidV4().slice(0, 4);
try {
const newActivity = await createActivityService({ ...data, author, uuid }, content);
return new SuccessRes(newActivity);
} catch (error) {
return new ErrorRes(createActivityErrorFailInfo);
}
}The service first creates the activity content in MongoDB, obtains the generated _id , then creates the activity record in MySQL with that contentId .
async function createActivityService(data = {}, content = {}) {
const { components = [], props = {}, setting = {} } = content;
const newContent = await ActivityContentModel.create({ components, props, setting });
const { _id: contentId } = newContent;
const newActivity = await ActivityModel.create({ ...data, contentId: contentId.toString() });
return newActivity.dataValues;
}Component Platform Backend Design
The component platform focuses on displaying reusable components. Main APIs:
POST /component – add component
GET /component/list – list components
GET /component/:id – component detail
PATCH /component/:id – update component
DELETE /component/:id – delete component
Data Table Design
Component Detail Table
Column
Type
Comment
component_id
int
Component ID
version
varchar
Version number
description
varchar
Description
author
varchar
Component author
coverImg
varchar
Component cover image
config
varchar
Component configuration
markdow
varchar
Component documentation
create_time
date
Creation time
update_time
date
Update time
Component List Table
Column
Type
Comment
id
int
Unique identifier
name
varchar
Component name
create_time
date
Creation time
update_time
date
Update time
frequency
int
Usage frequency
author
varchar
Component author
status
int
Status
The component APIs also follow a standard RESTful style.
C‑End Backend Design
The C‑end serves the final marketing H5 pages. Design steps:
Fetch page configuration (overall settings, component data) by page_id .
Determine activity timing and status to decide display logic.
Render the page on the server using the component library.
Server‑side rendering (SSR) is preferred for high traffic and fast first‑paint requirements. Unlike client‑side rendering, SSR returns a fully rendered HTML string, eliminating the need for the browser to download and execute large JavaScript bundles before displaying content.
Image source: "The Benefits of Server Side Rendering Over Client Side Rendering".
Client‑Side Rendering
The browser first receives an empty HTML page, then loads JavaScript and CSS, requests data via AJAX, and finally renders the DOM, causing a noticeable white‑screen period.
Server‑Side Rendering
SSR generates the complete HTML on the Node server using a bundle renderer, sends it to the browser, and then hydrates it with the client‑side JavaScript bundle for interactivity.
Vue SSR is used as an example: the source code is bundled into vue-ssr-server-bundle.json and vue-ssr-client-manifest.json . The server creates a renderer with createBundleRenderer and calls renderer.renderToString to produce the HTML string, which is sent to the browser. The client then hydrates this HTML with the generated JavaScript to make the page interactive.
For more details, refer to the previous articles on the necessity and principles of SSR for To‑C marketing pages.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.