Learn how to start a fresh project with 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.
Before we setup your first project, make sure you have a basic development environment setup, with the following tools installed on your system:
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)
tensei()
method creates a new tensei appplugins()
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..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
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.
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.
In the terminal you can locate the <a href="..."></a>
and Cmd + Click
on it to open it in the browser.
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'),
])
...
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'),
])
text()
field creates a text field in the CMS and API.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.textarea()
field creates a larger text field. On the CMS, it'll show up as a textarea.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.
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.
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.
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.
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:
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]
post
query fetches a single post by IDposts
query fetches, orders, paginates and filters all postspostsCount
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:
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:
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)