Being a beginner means exploring new horizons in programming, stepping into the unknown, hoping that somewhere there will be better.
I think you will agree that it is often quite fun to start a project with new technology. The problems you face and try to solve are not always easy, although they are integral to your journey to becoming a guru.
So what am I talking about. Today I am here to share with you my first experience of creating a system from Hedless CMS, API and blog. Due to the lack of a sufficient amount of such material, especially in Russian, I hope that this article will help you create such a system on your own, avoiding the mistakes that I made.
I will tell you how I assembled the system in blocks and what came of it. I won't explain the background information, but I will leave links to resources where you can learn more. Sometimes it is difficult to find a Russian-language source, but I will try. In addition, you can watch the talk (in English) or read this article (the closest in meaning) if you are not sure about the advantages of microservices over monolithic architecture.
API ( , ):
Vidzhel/Bluro
, , , - , - . .
( ) . , , , «» . .
, , . . - , .
, , . , Headless () CMS, Bluro. «Hello world» , «TechOverload» .
-, , , .
, . . . , , , , .
, :
, ,
, , ,
, ,
,
, ,
, , , , :
, , . , , . , , : , , Headless CMS, , .
- , Python Django. , , .
, YouTube, .
, , . — , URL (, ). - .
, , . , , .
API. - , .
JavaScript, NodeJS React . , .
Bluro CMS
Headless CMS , (UI). , . CMS API (REST API , ), .
, , , API — , — , . , , , , URL-, , .
, http . , , .
— MVC (Model View Controller). ( ).
, , , , .
CMS .
, - API, CMS. , , , , .
- , .
. .
Main , .
ORM
, — ORM (Object Relational Mapper).
, , , - ? , , . , . , — .
— «». , , SQL .
, , . : (, ), ( ), , . , , . , - « ».
. , Model ( ), . Model . , , .
, ORM. , .
. , , . . , , - . , , - , . , - : ).
, . Sequelize API Django, . ORM.
Entities — , ( , ). Model QuerySet , . , QuerySet Statement, API . StatementsBuilder — , Statement . , .
« », , .
, , . , , , ORM.
ORM. , .
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const Model = DependencyResolver.getDependency(null, "Model");
const ARTICLE_STATES = {
PUBLISHED: "PUBLISHED",
PENDING_PUBLISHING: "PENDING_PUBLISHING",
};
const VERBOSE_REGEXP = /^[0-9a-z-._~]*$/i;
class Article extends Model {
static STATES = ARTICLE_STATES;
// There can be other methods
// that fetch data for you or process it in some way
}
// Define model with schema
Article.init([
{
columnName: "user",
foreignKey: {
table: "User",
columnName: "id",
onDelete: Model.OP.CASCADE,
onUpdate: Model.OP.CASCADE,
},
type: Model.DATA_TYPES.INT(),
},
{
columnName: "dateOfPublishing",
verboseName: "Date of publishing",
type: Model.DATA_TYPES.DATE_TIME(),
nullable: true,
validators: Model.CUSTOM_VALIDATORS_GENERATORS.dateInterval(),
},
{
columnName: "dateOfChanging",
verboseName: "Date of changing",
type: Model.DATA_TYPES.DATE_TIME(),
validators: Model.CUSTOM_VALIDATORS_GENERATORS.dateInterval(),
},
...
{
columnName: "state",
verboseName: "Article state",
type: Model.DATA_TYPES.VARCHAR(18),
possibleValues: Object.values(ARTICLE_STATES),
},
]);
// Somewhere else
const set = await Article.selector
.orderBy({ dateOfPublishing: "DESC" })
.limit(offset, count)
.filter({
firstValue: "dateOfChanging",
operator: Operators.between,
innerCondition: {
firstValue: "10.11.2020",
operator: Operators.and,
secondValue: "11.11.2020",
},
})
.filter({
user: "userId",
state: Article.STATES.PUBLISHED,
})
.fetch();
const resulte = await set.getList();
const Model = DependencyResolver . getDependency ( null , "Model" ) ;
const ARTICLE_STATES = {
PUBLISHED : "PUBLISHED" ,
PENDING_PUBLISHING : "PENDING_PUBLISHING" ,
} ;
const VERBOSE_REGEXP = /^ [ 0-9a-z-._~] * $ /i ;
class Article extends Model {
static STATES = ARTICLE_STATES ;
// There can be other methods
// that fetch data for you or process it in some way
}
// Define model with schema
Article . init ( [
{
columnName : "user" ,
foreignKey : {
table : "User" ,
columnName : "id" ,
onDelete : Model . OP . CASCADE ,
onUpdate : Model . OP . CASCADE ,
} ,
type : Model . DATA_TYPES . INT ( ) ,
} ,
{
columnName : "dateOfPublishing" ,
verboseName : "Date of publishing" ,
type : Model . DATA_TYPES . DATE_TIME ( ) ,
nullable : true ,
validators : Model . CUSTOM_VALIDATORS_GENERATORS . dateInterval ( ) ,
} ,
{
columnName : "dateOfChanging" ,
verboseName : "Date of changing" ,
type : Model . DATA_TYPES . DATE_TIME ( ) ,
validators : Model . CUSTOM_VALIDATORS_GENERATORS . dateInterval ( ) ,
} ,
...
{
columnName : "state" ,
verboseName : "Article state" ,
type : Model . DATA_TYPES . VARCHAR ( 18 ) ,
possibleValues : Object . values ( ARTICLE_STATES ) ,
} ,
] ) ;
// Somewhere else
const set = await Article . selector
. orderBy ( { dateOfPublishing : "DESC" } )
. limit ( offset , count )
. filter ( {
firstValue : "dateOfChanging" ,
operator : Operators . between ,
innerCondition : {
firstValue : "10.11.2020" ,
operator : Operators . and ,
secondValue : "11.11.2020" ,
} ,
} )
. filter ( {
user : "userId" ,
state : Article . STATES . PUBLISHED ,
} )
. fetch ( ) ;
const resulte = await set . getList ( ) ;
. , .
, , . , . , , Django.
, CMS, , . , , , . , , . , . , , , .
GIT, , .
. , .
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{
"migrated": true,
"initialMigration": true,
"tables": [
[
"User",
{
"migrated": false,
"DEFINE_TABLE": true,
"DEFINE_COLUMN": {
"userName": {
"name": "userName",
"type": { "id": "VARCHAR", "size": 10 },
"default": null,
"nullable": false,
"autoincrement": false,
"primaryKey": false,
"unique": false,
"foreignKey": null
},
"password": {
"name": "password",
"type": { "id": "VARCHAR", "size": 10 },
"default": null,
"nullable": false,
"autoincrement": false,
"primaryKey": false,
"unique": false,
"foreignKey": null
},
"id": {
"name": "id",
"type": { "id": "INT" },
"default": null,
"nullable": false,
"autoincrement": true,
"primaryKey": true,
"unique": false,
"foreignKey": null
}
}
}
]
],
"name": "0_Auth_migration.json"
}
{
" migrated" : true ,
" initialMigration" : true ,
" tables" : [
[
" User" ,
{
" migrated" : false ,
" DEFINE_TABLE" : true ,
" DEFINE_COLUMN" : {
" userName" : {
" name" : " userName" ,
" type" : { " id" : " VARCHAR" , " size" : 10 },
" default" : null ,
" nullable" : false ,
" autoincrement" : false ,
" primaryKey" : false ,
" unique" : false ,
" foreignKey" : null
},
" password" : {
" name" : " password" ,
" type" : { " id" : " VARCHAR" , " size" : 10 },
" default" : null ,
" nullable" : false ,
" autoincrement" : false ,
" primaryKey" : false ,
" unique" : false ,
" foreignKey" : null
},
" id" : {
" name" : " id" ,
" type" : { " id" : " INT" },
" default" : null ,
" nullable" : false ,
" autoincrement" : true ,
" primaryKey" : true ,
" unique" : false ,
" foreignKey" : null
}
}
}
]
],
" name" : " 0_Auth_migration.json"
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{
"migrated": true,
"initialMigration": false,
"tables": [
[
"User",
{
"migrated": false,
"CHANGE_COLUMN": {
"password": {
"name": "password",
"type": {
"id": "VARCHAR",
"size": 50
},
"default": null,
"nullable": false,
"autoincrement": false,
"primaryKey": false,
"unique": false,
"foreignKey": null
}
}
}
]
],
"name": "1_Auth_migration.json"
}
{
" migrated" : true ,
" initialMigration" : false ,
" tables" : [
[
" User" ,
{
" migrated" : false ,
" CHANGE_COLUMN" : {
" password" : {
" name" : " password" ,
" type" : {
" id" : " VARCHAR" ,
" size" : 50
},
" default" : null ,
" nullable" : false ,
" autoincrement" : false ,
" primaryKey" : false ,
" unique" : false ,
" foreignKey" : null
}
}
}
]
],
" name" : " 1_Auth_migration.json"
}
Server
, http-. , , . HTTP, : Request Response, .
Request , , multipart / form-data.
Response , . , cookie.
Router
«» — , . Express — , , , .
Route — , . , , . .
Rule — , . , authorizationRule, , . , . , . Rule , , Rule Route.
. , ( ), .
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
connectRule("all", "/", authRule, { sensitive: false });
connectRule(["put", "delete"], "/profiles/{verbose}", requireAuthorizationRule);
connectRule(["post", "delete"], "/profiles/{user}/followers", requireAuthorizationRule);
connectRoute("get", "/profiles", getProfilesController);
connectRoute("put", "/profiles/{verbose}", updateProfileController);
connectRule ( "all" , "/" , authRule , { sensitive : false } ) ;
connectRule ( [ "put" , "delete" ] , "/profiles/{verbose}" , requireAuthorizationRule ) ;
connectRule ( [ "post" , "delete" ] , "/profiles/{user}/followers" , requireAuthorizationRule ) ;
connectRoute ( "get" , "/profiles" , getProfilesController ) ;
connectRoute ( "put" , "/profiles/{verbose}" , updateProfileController ) ;
, . , API. , , , , .
, API, , . , , , .
. — - , . Modules Manager, , , , . , .
, SOLID, . , , . , , . - , .
.
API
, API . , , . , , .
Auth
, , : , , , ...
API , . , , .
, JWT (JSON Web Token) cookie. . .
, :
authRule — , cookie . , , .
requireAuthorizationRule — , .
Article
, . , .
.
Notifications
.
NotificationService .
API:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{
"email": "email",
"pass": "password"
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{
"session": {
"verbose": "id that is used to get profile info",
"userName": "userName",
"role": "user role: 'ADMIN', 'USER'",
"email": "email"
},
"errors": "error's descriptions list",
"success": "success's descriptions list",
"info": "info's descriptions list",
"notifications": [
"collection of notifications"
]
}
{
" email" : " email" ,
" pass" : " password"
}
{
" session" : {
" verbose" : " id that is used to get profile info" ,
" userName" : " userName" ,
" role" : " user role: 'ADMIN', 'USER'" ,
" email" : " email"
},
" errors" : " error's descriptions list" ,
" success" : " success's descriptions list" ,
" info" : " info's descriptions list" ,
" notifications" : [
" collection of notifications"
]
}
CMS, , , . React .
- , . « » . React Router . , -. , , , -, .
Redux "" Redux-Saga ( Redux-Saga ). , Redux (Action), . (Reducer) , - , , .
, Redux-Saga , , . , .
Redux-Saga, Headless CMS. , :
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function* fetchData(endpoint, requestData) {
const controller = new AbortController();
const { signal } = controller;
let res, wasTimeout, reason, failure;
failure = false;
try {
// use Fetch API to make request, wait no longer than `TIMEOUT`
const raceRes = yield race([
call(fetch, endpoint, {
...requestData,
signal,
mode: "cors",
redirect: "follow",
credentials: "include",
}),
delay(TIMEOUT, true),
]);
res = raceRes[0];
wasTimeout = raceRes[1] || false;
if (wasTimeout) {
failure = true;
reason = "Connection timeout";
// Abort fetching
controller.abort();
}
} catch (e) {
console.log(e);
reason = "Error occurred";
}
return { reason, res, failure, wasTimeout };
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function* makeRequest(endpoint, requestData) {
// Signal that we start making request (we can use it to show loading wheel)
yield put({ type: SES_ASYNC.START_MAKING_REQUEST_ASYNC });
// call enother saga that will make request
let { res, reason, failure, wasTimeout } = yield call(fetchData, endpoint, requestData);
if (res) {
// Process response
const results = yield call(handleResponse, res, wasTimeout, reason, failure);
// Signal about finishing
yield put({ type: SES_ASYNC.END_MAKING_REQUEST_ASYNC });
return results;
} else {
// Return error
failure = true;
reason = "Server error";
yield put({ type: SES_ASYNC.END_MAKING_REQUEST_ASYNC });
return { res: null, wasTimeout, reason, data: null, failure };
}
}
function * fetchData ( endpoint , requestData ) {
const controller = new AbortController ( ) ;
const { signal } = controller ;
let res , wasTimeout , reason , failure ;
failure = false ;
try {
// use Fetch API to make request, wait no longer than `TIMEOUT`
const raceRes = yield race ( [
call ( fetch , endpoint , {
...requestData ,
signal,
mode : "cors" ,
redirect : "follow" ,
credentials : "include" ,
} ) ,
delay ( TIMEOUT , true ) ,
] ) ;
res = raceRes [ 0 ] ;
wasTimeout = raceRes [ 1 ] || false ;
if ( wasTimeout ) {
failure = true ;
reason = "Connection timeout" ;
// Abort fetching
controller . abort ( ) ;
}
} catch ( e ) {
console . log ( e ) ;
reason = "Error occurred" ;
}
return { reason, res, failure, wasTimeout } ;
}
export function * makeRequest ( endpoint , requestData ) {
// Signal that we start making request (we can use it to show loading wheel)
yield put ( { type : SES_ASYNC . START_MAKING_REQUEST_ASYNC } ) ;
// call enother saga that will make request
let { res, reason, failure, wasTimeout } = yield call ( fetchData , endpoint , requestData ) ;
if ( res ) {
// Process response
const results = yield call ( handleResponse , res , wasTimeout , reason , failure ) ;
// Signal about finishing
yield put ( { type : SES_ASYNC . END_MAKING_REQUEST_ASYNC } ) ;
return results ;
} else {
// Return error
failure = true ;
reason = "Server error" ;
yield put ( { type : SES_ASYNC . END_MAKING_REQUEST_ASYNC } ) ;
return { res : null , wasTimeout, reason, data : null , failure } ;
}
}
fetchData — , Fetch API . , TIMEOUT, . makeRequest , . - . , , :
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function* openArticle({ verbose }) {
// Get cached articles from the state
const article = yield select(getFetchedArticle, verbose);
// If we don't have this article in cache, fetch it
if (!article) {
const { failure, data } = yield call(
makeRequest,
`${configs.endpoints.articles}/${verbose}`,
{
method: "GET",
},
);
if (!failure) {
article = yield call(convertArticleData, data.entry);
}
}
// If article was successfuly fetched, we signaling to open it
if (article) {
yield fork(fetchArticleContent, { fileName: article.textSourceName });
yield put({
type: ART_ASYNC.OPEN_ARTICLE_ASYNC,
article,
});
}
}
function * openArticle ( { verbose } ) {
// Get cached articles from the state
const article = yield select ( getFetchedArticle , verbose ) ;
// If we don't have this article in cache, fetch it
if ( !article ) {
const { failure, data } = yield call (
makeRequest ,
`${ configs . endpoints . articles } /${ verbose } ` ,
{
method : "GET" ,
} ,
) ;
if ( !failure ) {
article = yield call ( convertArticleData , data . entry ) ;
}
}
// If article was successfuly fetched, we signaling to open it
if ( article ) {
yield fork ( fetchArticleContent , { fileName : article . textSourceName } ) ;
yield put ( {
type : ART_ASYNC . OPEN_ARTICLE_ASYNC ,
article,
} ) ;
}
}
, . ( ).
. — .
- NGINX:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
server {
listen 80;
client_max_body_size 100M;
location / {
proxy_pass http://front_blog:3000;
}
location /admin {
proxy_pass http://front_admin_panel:3000;
}
location /api {
rewrite ^/api/?(.*)$ /$1 break;
proxy_pass http://bluro_api:8000;
}
}
server {
listen 80;
client_max_body_size 100M;
location / {
proxy_pass http://front_blog:3000;
}
location /admin {
proxy_pass http://front_admin_panel:3000;
}
location /api {
rewrite ^/api/?(.*)$ /$1 break;
proxy_pass http://bluro_api:8000;
}
}
Docker Compose, . , ( — ).
- , headlesscms.org , Headless CMS , .
, , , -.