Create a headless CMS using OceanBase and TypeScript: A step-by-step tutorial
Tags: typescript, oceanbase, database, cms
Published: 2023-12-07 12:07:41

If you're planning to start a blog or showcase your products on a website, you have two main options. You could code everything from scratch using HTML, CSS, and JavaScript, creating databases and interfaces to manage your content. This, however, can be challenging if you're not a seasoned programmer. A more efficient alternative is to use a Content Management System (CMS), providing you with the tools to manage your content and design your website effortlessly.

There are numerous CMSs available, each with its strengths. WordPress, the most popular CMS, is known for its user-friendly interface and vast plugin ecosystem. Joomla and Drupal offer more robust platforms for complex websites, though they require some technical expertise. For beginners, Squarespace and Wix are ideal for creating visually attractive websites without needing to code.

However, these CMSs are often tied to the presentation layer, limiting your content's reach to the platform they serve. If you want your content to reach audiences through various channels like mobile apps, smart devices, or voice assistants, traditional CMSs may fall short. The modern web extends beyond websites to include mobile and IoT devices, smartwatches, and in-car entertainment systems.

## What is headless CMS?

Enter the world of headless CMS. A headless CMS takes a different approach, separating the content management from the content presentation. Imagine it as a conductor, orchestrating your content delivery to various platforms without being tied to a single instrument. 

A headless CMS is a back-end content repository system that facilitates content storage, modification, and delivery via a RESTful API (Application Programming Interface) or GraphQL. It is called 'headless' because it decouples the 'head' (the front-end or the presentation layer) from the 'body' (the back-end or the content repository).

A headless CMS brings several benefits to the table. It gives developers the freedom to use their preferred tech stack for front-end development, fostering unique and tailored user experiences. It supports omnichannel content delivery, a critical factor in today's multi-device world. Yet, it's not without its challenges. The absence of a visual front-end may make it less intuitive for non-technical users. 

It also demands a proficient development team to build the client side, and certain features like SEO and preview functionality may require extra effort to implement. Despite these challenges, a headless CMS, with its flexibility and adaptability, stands as a formidable tool in the right hands.

## Building a headless CMS

Now that we've navigated the landscape of CMS, it's time to get our hands dirty and dive into the heart of this article: building a headless CMS using TypeScript and OceanBase.

Why this particular tech stack, you might ask? 

Let's start with [TypeScript](https://www.typescriptlang.org/). It's a superset of JavaScript that introduces static typing, a feature that JavaScript traditionally lacks. Static typing enhances code reliability, predictability, and maintainability, making TypeScript a powerful part of our stack to build a robust headless CMS.

You might have come across [blog posts](https://www.simplilearn.com/tutorials/programming-tutorial/backend-languages) hailing JavaScript as the "most popular back-end language for web development." But in reality, pure JavaScript is seldom used for back-end development. More often than not, developers lean towards TypeScript, JavaScript’s statically typed sibling. The reason for this preference lies in TypeScript's unique features that enhance code reliability and maintainability, making it a more suitable choice for back-end development.

Next up is [OceanBase](https://en.oceanbase.com/), my choice of database for most of my projects. As an enterprise-grade, high-performance relational database, OceanBase provides a strong backbone for the headless CMS. Its high scalability and reliability ensure that our content management system can handle vast amounts of data and traffic, making it a reliable choice for large-scale applications.

Finally, I’ve chosen [Express.js](https://expressjs.com/) to build the API server. Express is a flexible Node.js web application framework that provides a robust set of features for web and mobile applications. Its simplicity and flexibility make it an ideal choice for the CMS project, allowing us to build powerful APIs with minimal hassle.

The codebase of the project has been uploaded to this [GitHub repo](https://github.com/wayneshn/oceanbase-typescript). You can clone and play around with it, or deploy it with GitPod and see it work in action. 

## Setting up the project

Before we start, ensure that you have the following installed on your machine:

- [Node.js](https://nodejs.org/en/): This is the JavaScript runtime we'll be using to run our server-side code.
- [TypeScript](https://www.typescriptlang.org/): As we discussed earlier, TypeScript is a superset of JavaScript with static type checking.
- [OceanBase](https://en.oceanbase.com/): To use OceanBase in the project, you first need a running OceanBase cluster. You have several options for doing so. You can [install OceanBase in your local environment](https://en.oceanbase.com/docs/community-observer-en-10000000000829647), [spin up a virtual machine in the cloud](https://medium.com/oceanbase-database/how-to-install-oceanbase-on-an-aws-ec2-instance-step-by-step-guide-aab852c2e0a7) to run it or use [OceanBase Cloud in the AWS marketplace](https://aws.amazon.com/marketplace/pp/prodview-d2evwth3ztaja) to set up your cluster in just a few clicks. After setting up your OceanBase server, don’t forget to obtain the username, password, hostname, and port for later use.

First, let's initialize our project. Open your terminal, navigate to your desired directory, and run the following command to create a new Node.js project:

```bash
$ mkdir oceanbase-typescript && cd oceanbase-typescript
$ npm init -y
```

This will create a new directory for your project and initialize a new Node.js application inside it.

Next, let's install the necessary dependencies. We'll need *`express`* for our server, TypeScript for writing our code, and a couple of other packages:

```bash
$ npm install express typescript ts-node nodemon
```

In this project, we are going to use TypeORM as the Object-Relational Mapping (ORM) to connect our project with OceanBase. TypeORM supports a wide variety of databases, including MySQL, Postgresql, and MongoDB. Since TypeORM doesn’t yet have a driver for OceanBase, and OceanBase is compatible with MySQL, we are going to use the MySQL driver for our OceanBase connection.

To use TypeORM and the MySQL driver, we need to install them in our project. Run the following command:

```bash
$ npm install typeorm mysql
```

Now, let's create an `src/index.ts` file in the root directory of our project. This will be the entry point of our application. Here's a basic setup for our Express server:

```jsx
import express from 'express';

const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});
```

To run the server, we'll use `ts-node` in conjunction with `nodemon` to ensure our server restarts whenever we make changes. Add a `start` script to your `package.json`:

```json
"scripts": {
    "start": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
    // ... other scripts
}
```

Now, you can start your server by running:

```bash
npm run start
```

You should see the console log `Server is running on port 3000`. Now, make a GET request to the server at `http://localhost:3000`, you should see "Hello World!" displayed.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h5eajzlf3904v9mtz1w.png)


That's it! You've now set up a basic TypeScript and express.js project. In the next section, we'll connect to our OceanBase database and start building the CMS functionalities.

## Connection to OceanBase

Our CMS will consist of blog posts, tags, and users. Each of these corresponds to an entity in our application. Let's create each of these entities.

Create the following files in the `src/entity` directory:

- `User.ts`
- `BlogPost.ts`
- `Tag.ts`

Let's start with `User.ts`. A user in our CMS will have an ID, a name, and a list of blog posts they've authored. Here's how you can define this entity:

```jsx
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"

@Entity()
export class User {

    @PrimaryGeneratedColumn()
    id: number

    @Column()
    firstName: string

    @Column()
    lastName: string

    @Column()
    age: number

}
```

Next, we'll define the `Tag` entity in `Tag.ts`. A tag will have an ID, a name, and a list of blog posts that have this tag.

```jsx
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
} from "typeorm";
import { BlogPost } from "./BlogPost";

@Entity()
export class Tag {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @ManyToMany(() => BlogPost, blogPost => blogPost.tags)
    blogPosts: BlogPost[];
}
```

Finally, we'll define the `BlogPost` entity in `BlogPost.ts`. A blog post will have an ID, a title, a slug (for the URL), the content of the post, a reference to the author, and a list of tags.

```jsx
import {
    Entity,
    PrimaryGeneratedColumn,
    Column,
    ManyToMany,
    JoinTable
} from "typeorm";
import { Tag } from "./Tag";

@Entity()
export class BlogPost {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    content: string;

    @Column()
    author: string;

    @Column()
    excerpt: string;

    @Column({
        unique: true
    })
    slug: string;

    @Column({
        type: 'date'
    })
    publishDate: Date;

    @Column({
        type: 'enum',
        enum: ['DRAFT', 'PUBLISHED', 'ARCHIVED'],
        default: 'DRAFT'
    })
    status: string;

    @ManyToMany(() => Tag, tag => tag.blogPosts, { 
        cascade: true
    })
    @JoinTable()
    tags: Tag[];
}
```

Now that we have set up our entities, the next step is to connect our application to the OceanBase database. For this, we will create a `DataSource` object that will handle the connection.

Let's have a look at the `src/data-source.ts` file:

```jsx
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"
import {Tag} from './entity/Tag'
import { BlogPost } from "./entity/BlogPost"

require('dotenv').config()

export const AppDataSource = new DataSource({
    type: "mysql",
    host: process.env.OB_HOST,
    port: Number(process.env.OB_PORT),
    username: process.env.OB_USER,
    password: process.env.OB_PASSWORD,
    database: process.env.OB_DB,
    synchronize: true,
    logging: false,
    entities: [User, Tag, BlogPost],
    migrations: [],
    subscribers: [],
}).initialize()
```

We first import the necessary modules and entities. Then, we use the `dotenv` package to load environment variables from a `.env` file. This is where we'll store our OceanBase connection details.

Next, we create a new `DataSource` object. This object is configured to connect to a MySQL database, which is compatible with OceanBase. We provide it with the necessary connection details:

- `host`: The host where your OceanBase cluster is running.
- `port`: The port to connect to.
- `username` and `password`: The credentials to authenticate with the database.
- `database`: The name of the database you want to connect to.

Lastly, we specify the entities that we created earlier. These are the tables that TypeORM will create in our OceanBase database.

Now, wherever we need to interact with the database in our application, we can import this `AppDataSource` and use it. This way, we ensure that our application is always using the same database connection.

Remember to replace the placeholders in the `.env` file with your actual OceanBase connection details. The `.env` file should look something like this:

```bash
OB_HOST=your-oceanbase-host
OB_PORT=your-oceanbase-port
OB_USER=your-oceanbase-username
OB_PASSWORD=your-oceanbase-password
OB_DB=your-oceanbase-database
```

With this setup, our application is now ready to store and retrieve data from OceanBase. In the next section, we'll start building the API endpoints for our CMS.

## Setting up CRUD actions for the CMS

The core of our CMS will be the CRUD (Create, Read, Update, Delete) actions. These actions will allow us to manage our content, including creating blog posts, adding tags, and retrieving posts. In this section, we will create several modules to handle these actions.

### Add a tag

The first module we will create is `addTag.ts`, which will allow us to add new tags to our CMS. This module imports the `AppDataSource` and `Tag` entity and exports an asynchronous function `addTag` that takes a tag name as a parameter.

```tsx
import { AppDataSource } from "../data-source";
import { Tag } from "../entity/Tag";

export async function addTag(name: string) {
    const dataSource = await AppDataSource;
    const tagRepository = dataSource.manager.getRepository(Tag);

    const newTag = new Tag();
    newTag.name = name;

    const result = await tagRepository.save(newTag);

    return result;
}

```

This function first waits for the `AppDataSource` to initialize, then fetches the repository for the `Tag` entity. It creates a new `Tag` instance, assigns the provided name to it, and saves this new tag to the repository. The function then returns the result of this operation.

### Create a post

Next, we'll create `createPost.ts`, which will allow us to create blog posts. This module is similar to `addTag.ts`, but it also handles the creation of tags associated with the post.

```tsx
import { AppDataSource } from "../data-source";
import { BlogPost } from "../entity/BlogPost";
import { Tag } from "../entity/Tag";

export async function createPost(title: string, content: string, author: string, slug: string, publishDate: Date, status: string, tagNames: string[]) {
    const dataSource = await AppDataSource;
    const postRepository = dataSource.manager.getRepository(BlogPost);
    const tagRepository = dataSource.manager.getRepository(Tag);

    const tags = [];
    for (const tagName of tagNames) {
        let tag = await tagRepository.findOne({ where: { name: tagName } });
        if (!tag) {
            tag = new Tag();
            tag.name = tagName;
            tag = await tagRepository.save(tag);
        }
        tags.push(tag);
    }

    const newPost = new BlogPost();
    newPost.title = title;
    newPost.content = content;
    newPost.author = author;
    newPost.slug = slug;
    newPost.publishDate = publishDate;
    newPost.status = status;
    newPost.tags = tags;

    return await postRepository.save(newPost);
}
```

This function takes several parameters: `title`, `content`, `author`, `slug`, `publishDate`, `status`, and `tagNames`. It initializes the `AppDataSource` and fetches the repositories for `BlogPost` and `Tag`. It then creates a new `Tag` for each tag name provided, if it doesn't already exist, and saves it to the `Tag` repository.

A new `BlogPost` is then created with the provided parameters and the array of `Tag` instances. The post is saved to the `BlogPost` repository and the function returns the result of this operation.

### Get all posts

The `getAllPosts.ts` module allows us to retrieve all blog posts from our CMS. It exports an asynchronous function `getAllPosts` that fetches and returns all posts from the `BlogPost` repository.

```bash
import { AppDataSource } from "../data-source";
import { BlogPost } from "../entity/BlogPost";

export async function getAllPosts() {
    const dataSource = await AppDataSource;
    return await dataSource.manager.getRepository(BlogPost).find();
}
```

Similarly, the `getAllTags.ts` module exports an asynchronous function `getAllTags` that fetches and returns all tags from the `Tag` repository.

```bash
import { AppDataSource } from "../data-source";
import { Tag } from "../entity/Tag";

export async function getAllTags() {
    const dataSource = await AppDataSource;
    return await dataSource.manager.getRepository(Tag).find();
}
```

### Get a post by slug

The `getPostBySlug.ts` module exports an asynchronous function `getPostBySlug` that fetches and returns a post based on its slug.

```bash
import { AppDataSource } from "../data-source";
import { BlogPost } from "../entity/BlogPost";

export async function getPostBySlug(slug: string) {
    const dataSource = await AppDataSource;
    return await dataSource.manager.getRepository(BlogPost).findOne({where: {slug}});
}
```

### Get all posts by tag

The `getPostsByTag.ts` module exports an asynchronous function `getPostsByTag` that fetches and returns all posts associated with a specific tag.

```bash
import { AppDataSource } from "../data-source";
import { BlogPost } from "../entity/BlogPost";
import { Tag } from "../entity/Tag";

export async function getPostsByTag(tagName: string) {
    const dataSource = await AppDataSource;
    const tagRepository = dataSource.manager.getRepository(Tag);
    const tag = await tagRepository.findOne({ where: { name: tagName } });

    if (!tag) {
        throw new Error(`No tag found with the name ${tagName}`);
    }

    return await dataSource.manager.getRepository(BlogPost).find({ where: { tags: tag } });
}
```

This function takes a `tagName` as a parameter. It initializes the `AppDataSource` and fetches the repository for `Tag`. It then searches for a tag with the provided name. If no such tag is found, an error is thrown. If the tag is found, the function fetches and returns all `BlogPost` entities associated with this tag from the `BlogPost` repository.

With these files, we have set up the basic CRUD operations for our CMS. These operations allow us to create, retrieve, and manage blog posts and tags in our CMS, utilizing the power of TypeScript and OceanBase.

## Setting up the API server in Express

The Express.js framework will handle the HTTP requests and responses for our CMS. In the `src/index.ts` file, we start by importing the necessary modules and functions:

```tsx
import express, { Request, Response , NextFunction} from 'express';
import { AppDataSource } from './data-source';
import { addTag } from './modules/addTag';
import { createPost } from './modules/createPost';
import { getAllPosts } from './modules/getAllPosts';
import { getAllTags } from './modules/getAllTags';
import { getPostBySlug } from './modules/getPostBySlug';
import { getPostsByTag } from './modules/getPostsByTag';
```

Next, we create an Express application and set the port to `3000`. We also use the `express.json()` middleware to parse incoming JSON payloads:

```tsx
const app = express();
const port = 3000;

app.use(express.json());
```

We then define several routes for our CMS:

`GET /posts`: This route fetches all blog posts from the CMS and sends them in the HTTP response.

```tsx
app.get('/posts', async (req: Request, res: Response) => {
    const posts = await getAllPosts();
    res.json(posts);
});
```

`POST /posts`: This route creates a new blog post. It expects the post details to be provided in the HTTP request body.

```tsx
app.post('/posts', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const { title, content, author, slug, publishDate, status, tagNames } = req.body;
        await createPost(title, content, author, slug, publishDate, status, tagNames);
        res.sendStatus(201);
    } catch (error) {
        next(error);
    }
});
```

`POST /tags`: This route creates a new tag. It expects the tag name to be provided in the HTTP request body.

```tsx
app.post('/tags', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const { name } = req.body;
        await addTag(name);
        res.sendStatus(201);
    } catch (error) {
        next(error);
    }
});
```

`GET /tags`: This route fetches all tags from the CMS and sends them in the HTTP response.

```tsx
app.get('/tags', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const tags = await getAllTags();
        res.json(tags);
    } catch (error) {
        next(error);
    }
});
```

`GET /posts/:slug`: This route fetches a blog post based on its slug and sends it in the HTTP response.

```tsx
app.get('/posts/:slug', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const { slug } = req.params;
        const post = await getPostBySlug(slug);
        if (post) {
            res.json(post);
        } else {
            res.sendStatus(404);
        }
    } catch (error) {
        next(error);
    }
});
```

`GET /posts/tag/:tagName`: This route fetches all blog posts associated with a specific tag and sends them in the HTTP response.

```tsx
app.get('/posts/tag/:tagName', async (req: Request, res: Response, next: NextFunction) => {
    try {
        const { tagName } = req.params;
        const posts = await getPostsByTag(tagName);
        res.json(posts);
    } catch (error) {
        next(error);
    }
});
```

We also add an error handling middleware to catch any errors that might occur during the handling of requests:

```tsx
app.use((error: Error, req: Request, res: Response, next: NextFunction) => {
    console.error(error);
    res.status(500).json({ error: 'Internal server error' });
});
```

Finally, we start the Express server and add event listeners to handle graceful shutdowns:

```tsx
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

process.on('SIGINT', async () => {
    const conn = await AppDataSource;
    await conn.close();
    console.log('Database connection closed');
    process.exit(0);
});

process.on('unhandledRejection', (reason, promise) => {
    console.log('Unhandled Rejection at:', promise, 'reason:', reason);
    process.exit(1);
});
```

That's it! With these steps, we have set up the API server for our CMS using Express.js, TypeScript, and OceanBase.

## Testing the API

Now that we have our API server all set up, it's time to test it out. We will use the VS Code plugin [Thunder Client](https://www.thunderclient.com/) to make HTTP requests to our server and check its responses.

Type the following command to start the server.

```bash
npm run start
```

Let's start by testing the `POST /tags` endpoint that we created to add new tags to our CMS. Open Postman and create a new request. Set the request type to `POST` and the URL to `http://localhost:3000/tags`. In the request body, select `raw` and `JSON` and input a new tag name as follows:

```json
{
    "name": "Technology"
}
```

Click on "Send". If everything is working correctly, you should receive a 201 Created status code.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emoq53dtlb6bkvr1cbum.png)

Next, let's test the `POST /posts` endpoint. Create a new `POST` request to `http://localhost:3000/posts`. In the request body, input a new blog post with title, content, author, slug, publish date, status, and an array of tag names:

```json
{
    "title": "The Future of CMS",
    "content": "This is a blog post about the future of CMS.",
    "author": "John Doe",
    "slug": "the-future-of-cms",
    "publishDate": "2023-12-01",
    "status": "PUBLISHED",
    "tagNames": ["Technology", "CMS"]
}
```

Click on "Send". You should receive a `201 Created` status code.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b88xpol0nipm15nl53x7.png)

Now, let's test the `GET /posts` endpoint. Create a new `GET` request to `http://localhost:3000/posts` and click on "Send". You should receive a `200 OK` status code and a list of all blog posts in the response body.

![Untitled](https://prod-files-secure.s3.us-west-2.amazonaws.com/fdbd5cfb-94c3-423f-81f7-d5c54414216c/ff34161f-dda8-48e7-a4a2-ba8b62fde94e/Untitled.png)

Similarly, test the `GET /tags` endpoint by creating a new `GET` request to `http://localhost:3000/tags`. You should receive a `200 OK` status code and a list of all tags in the response body.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n6e3c9di0pvohymx8nsq.png)

To test the `GET /posts/tag/:tagName` endpoint, create a new `GET` request to `http://localhost:3000/posts/tag/Technology`. You should receive a `200 OK` status code and a list of all blog posts associated with the tag "Technology".


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/99siam05no4vf7xz9u3p.png)

Finally, to test the `GET /posts/:slug` endpoint, create a new `GET` request to `http://localhost:3000/posts/the-future-of-ai`. You should receive a `200 OK` status code and the details of the blog post with the slug "the-future-of-ai".


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ep74udjv9q71d53ahao5.png)

Congratulations! If all your requests returned the expected responses, your API server is working as expected. You've successfully built a headless CMS using TypeScript and OceanBase and equipped it with a robust API server using Express.js.

## Conclusion

Building a headless CMS might seem like a daunting task, but as we've seen in this tutorial, with the right tools and a solid structure, it's entirely achievable. Using TypeScript and OceanBase, we've created a flexible and scalable CMS that can serve content on a variety of platforms.

OceanBase, a distributed, high-performance relational database, served as the backbone of our CMS. Its high scalability and compatibility with MySQL made it an excellent choice for projects like this. OceanBase not only handled vast amounts of data efficiently but also provided reliability, ensuring that our CMS can handle large-scale applications.

TypeScript's static typing enhanced our code's reliability, predictability, and maintainability, making it a powerful ally in our quest to build a robust headless CMS. Express.js, a lightweight and flexible Node.js web application framework, enabled us to build a sleek API server with minimal hassle. 

This guide served as a demonstration of how you can use OceanBase in a TypeScript project. It's a testament to OceanBase's versatility and robustness, proving that it can be an excellent choice for a wide range of applications, from small projects to large-scale enterprise solutions.

Remember, the journey doesn't end here. There's a plethora of additional features and enhancements you can add to your CMS. From incorporating user authentication to adding a front end to interact with your CMS, the possibilities are endless. With OceanBase as the backbone of your project, you can be confident that your CMS will be able to handle whatever you throw at it. 

Again, you can find the source code of the project on this [GitHub repo](https://github.com/wayneshn/oceanbase-typescript). The repo can also be a starting point for any other TypeScript/OceanBase projects in your mind.