Getting started

Learn how to start a fresh project with Tensei.

What is Tensei?

Tensei is an elegant, open-source headless CMS that makes it very fast and easy to build powerful and secure APIs. Out of the box, you can setup a fully customisable GraphQL or Rest API with email/password and social authentication, JWT, access and refresh tokens, Role Based Access Control (RBAC) and a beautiful, fully customisable headless CMS dashboard to manage your content seamlessly.

Tensei supports Mysql, MongoDB, Sqlite and Postgresql. Database communication is implemented using the amazing Mikro ORM, a fast and secure ORM for Node.js.

Requirements

Before we setup your first project, make sure you have a basic development environment setup, with the following tools installed on your system:

Setup a new Tensei project

Let's build a simple blogging application. You can generate a project using a single command:

yarn create tensei-app blogging-app

# Or using npm
npx create-tensei-app blogging-app

This will generate a new project in a folder called blogging-app with GraphQL and authentication already setup for you. If you prefer a REST API, add the --rest parameter to the create command.

Tensei requires very little code to run, so you'll see only three files in the generated project: .gitignore, package.json and index.js file.

The index.js file contains your server, and the content should look like this:

import { cms } from '@tensei/cms'
import { auth } from '@tensei/auth'
import { media } from '@tensei/media'
import { graphql } from '@tensei/graphql'
import { tensei, welcome } from '@tensei/core'

tensei()
    .root(__dirname)
    .plugins([
        welcome(),
        cms().plugin(),
        media().graphql().plugin(),
        auth().plugin(),
        graphql().plugin()
    ])
    .start()
    .catch(console.error)
  • The tensei() method creates a new tensei app
  • The plugins() method registers plugins to your application. Each plugin is delegated to a specific task. This means you can remove plugins you do not need from your application easily.
    • .cms() registers the CMS dashboard.
    • .media() adds a simple media manager to the CMS for file uploads.
    • .auth() adds different authentication strategies to the application.
    • .graphql() adds a graphql API to the tensei app.
    • .welcome() adds the beautiful welcome screen to the / route.
  • The .start() method starts the server on port 8810.

Run the server with yarn dev. You should see the following message:

INFO    : 🚀 Access your server on http://localhost:8810
INFO    : 📉 Access your graphql playground on http://localhost:8810/graphql
INFO    : 🦄 Access your cms dashboard http://localhost:8810/cms

Introduction to CMS

The CMS uses passwordless authentication. Visit the CMS dashboard and type in an email for the first administrator user. Once you do, an email will be sent with a magic login link.

register admin tensei cms

Tensei supports multiple mail drivers, including console.log. The sent email is logged to your terminal with the magic link. CLick on it to login to the dashboard.

email html in terminal

In the terminal you can locate the <a href="..."></a> and Cmd + Click on it to open it in the browser.

Resources

For a blog, we'll need some resources such as Post, Comment, Category and Author. To define a resource, we may use the resource function from the @tensei/core package. Let's create the Post resource:

...
import { tensei, resource } from '@tensei/core'

tensei()
  .root(__dirname)
  .resources([
    resource('Post'),
  ])
  ...

Fields

A Post resource would need fields such as a title, description, and content. Each resource represents a database table. By default, the resource would have ID, Created At and Updated At fields. We need to add more fields to each registered resource. For example, the Post resource needs a Title, Slug, Description, Content, and Published At. Fields of different types are shipped with Tensei by default.

import { text, textarea, dateTime, slug } from '@tensei/core'

resource('Post')
  .fields([
      text('Title'),
      slug('Slug').from('Title'),
      textarea('Description'),
      textarea('Content'),
      dateTime('Published At'),
  ])
  • The text() field creates a text field in the CMS and API.
  • The slug() field creates a slug field from the Title. As you type in the Title field, it'll generate the slug dynamically. That's how powerful fields can be.
  • The textarea() field creates a larger text field. On the CMS, it'll show up as a textarea.
  • The dateTime() field creates a dateTime field and will show up as a date-time picker on the CMS.

Now save your changes, and visit the CMS dashboard. On the left sidebar, the Posts nav item should show up. The index view shows all the posts in the database. Now we have none yet. Click the Add Post button. This shows the form view, which you may use to insert a new Post.

add new post on tensei cms

Create a new Post. After creating, you're redirected to the index view. You should now have one post on the table. You may click the eye icon or the ID to view the details of the Post.

Let's add the Category resource to our blog.

import { text, textarea } from '@tensei/core'

resource('Category')
  .fields([
      text('Name'),
      textarea('Description'),
  ])

Create a new category and save. We should now have 1 post and 1 category on our dashboard.

Relationships

In our blog, we need to link posts to a category. To do this, we must establish a one to many relationship using the hasMany and belongsTo fields from tensei. We'll add the hasMany` field to the category resource:

import { text, textarea, hasMany } from '@tensei/core'

resource('Category')
  .fields([
      text('Name'),
      textarea('Description'),
      hasMany('Post')
  ])

Finally we'll add the belongsTo field to the post resource:

import { text, textarea, dateTime, slug, belongsTo } from '@tensei/core'

resource('Post')
  .fields([
      text('Title'),
      slug('Slug').from('Title'),
      textarea('Description'),
      textarea('Content'),
      dateTime('Published At'),
      belongsTo('Category')
  ])

Save your changes and refresh the cms page in the browser.Now visit the Post you created before. Notice there is a new field on the right side for Category. We may now select a category to associate with this post.

edit post with category

Save the changes. Now click the ID of the post to view its details. Notice there's a new Category field now showing a link to the associated category.

view post with category

If you click that link, it should take you directly to the view category page. You should now see a list of posts below the category, showing all posts associated with this category:

category view with post

GraphQL API

We may also perform CRUD operations on our data using the GraphQL API. The following queries are generated for the resources we have now:

type post {
  id: ID!
  created_at: String
  updated_at: String
  title: String
  slug: String
  description: String
  content: String
  published_at: String
  category: category
}

post(id: ID!): post
postsCount(offset: Int, limit: Int, where: PostWhereQuery): Int!
posts(offset: Int, limit: Int, where: PostWhereQuery, order_by: post_query_order): [post]
  • The post query fetches a single post by ID
  • The posts query fetches, orders, paginates and filters all posts
  • The postsCount query fetches, orders, filters all posts, and returns the count.

Visit the GraphQL Playground at http://localhost:8810/graphql and try the queries out like this:

post queries

Mutations for creating, updating and deleting posts are also generated automatically for you:

insert_post(object: insert_post_input!): post!
insert_posts(objects: [insert_post_input]!): [post]!

update_post(id: ID!, object: update_post_input!): post!
update_posts(where: PostWhereQuery!, object: update_post_input!): [post]!

delete_post(id: ID!): post
delete_posts(where: PostWhereQuery): [post]

Here's how you would create new a post using the insert_post mutation:

insert post mutation

Final Code

Here's the final 30 lines of code for our fully-featured blog with a CMS and CRUD GraphQL API:

import { cms } from '@tensei/cms'
import { auth } from '@tensei/auth'
import { media } from '@tensei/media'
import { graphql } from '@tensei/graphql'
import { tensei, welcome, resource, text, textarea, dateTime, slug, hasMany, belongsTo } from '@tensei/core'

tensei()
    .root(__dirname)
    .resources([
        resource('Post')
            .fields([
                text('Title'),
                slug('Slug').from('Title'),
                textarea('Description'),
                textarea('Content'),
                dateTime('Published At'),
                belongsTo('Category')
            ]),
            resource('Category')
                .fields([
                    text('Name'),
                    textarea('Description'),
                    hasMany('Post')
                ])
    ])
    .plugins([
        welcome(),
        cms().plugin(),
        media().plugin(),
        auth().plugin(),
        graphql().plugin()
    ])
    .start()
    .catch(console.error)