All in one guide for creating a killer API with Strapi

This article was originally published here

CMS has been around for quite some time. CMS refers to the content management system. Strapi is a headless CMS for Node.js. Strapi provides GUI for creating different content types and user management baked in the platform. Strapi supports both Restful API and GraphQL. Strapi supports both NoSQL and SQL databases. Changing the database is as simple as changing environment variables.

Setting up the work environment

Strapi requires Node.js installed on the system. Node.js can be downloaded from the official website. Strapi provides a boilerplate generator create-strapi-app, for setting up the application. It can be installed globally using npm using the following commands.

$ npm i -g create-strapi-app

Using create-strapi-app is simple just pass the name of the project. --quickstart will create a project with a default setting.

create-strapi-app my-blog --quickstart

An admin user is required to be created before Strapi can be used. Start the server using npm run develop.It starts the server on http://localhost:1337. Admin user is created using http://localhost:1337/admin/auth/register.

Example for Admin User

Once the boiler-plate is ready. Admin UI can be used to build database schema for API.

Backend

Strapi provides easy UI for the creation of database schema. For changing configuration, we have to edit out project files. For example, for changing the env variable we have to edit the config/environments folder.

Creating Database Schema

Strapi includes a content-builder plugin which provides a great UI for creating a database schema. The plugin is independent of the database. The same schema can be used in the SQL and NoSQL databases. The demo website would have a Blog Collection type and Comment Collection type. The blog would store most of the content of different articles. Comment collection will store comments on blog and user information.

Creating a Collection We will start by login into our admin at http://localhost:1337/admin. We now open the Content-Types builder page by clicking on Content-Types Builder from the sidebar.

Now create a new collection named “Blog”. This would store blogs for the site. It would have the title, image, and content.

Now we would create a collection named “Comment”. This would store comments for the blog. It would have content, user, blog. blog field store the link to the blog on which comment was created and user store details about the user who created it.

We have created links from comments, one to user collection, and other to blog collection. Blog collection and user collection don’t have information about the link. Now our backend is all set.

Documentation Plugin

We would install documentation-plugin from the “Marketplace” section for easy access to API details. This plugin would create the swagger specification for the API.

Authentication

Authentication is a very important aspect of any application. Strapi has JWT based authentication out of the box. A default key is used for signing JWT. Signing key can be changed in the configuration file /extensions/users-permissions/config/jwt.json. API for user signup and login is already baked in the platform.

{
"jwtSecret": "f1b4e23e-480b-4e58-923e-b759a593c2e0"
}

We will use the local provider for authentication. This password and email/username are used for authenticating a user. If we open “Documentation” from the Sidebar, it will provide an option to see the swagger API documentation.

We click on “Open the Documentation” to view swagger API Documentation. If we navigate to “UsersPermissions- User” we can see there are API to create a user and login user.

We will use /auth/local and /auth/local/register.

Setting Up Permission

Strapi has two roles by default. These roles are used to control access to content.

  1. Public role is for an unauthenticated user
  2. Authenticated is for an authenticated user.

These roles are automatically assigned to a user based on authentication status. We would allow “Public” users to read blogs and comments and “Authenticated” users can comment on the blog and edit comments. Roles can be edited in the “Roles and Permission” section.

Edit public Roles to allow access to Blogs and Comments

Adding Comments

Now let us add comments to our demo website. For adding comments a user must be authenticated. We need to control write access to comment collection by customizing controller for “Comment” collection. The controller for every collection is located in the api folder. For changing controller edit file api/comment/controllers/comment.js.

We need to install strapi-utils for editing our controller.

npm i strapi-utils
// file: api/comment/controllers/comment.js
const { sanitizeEntity } = require('strapi-utils');
module.exports = {
// this method is called when api to create comment is called
async create(ctx) {
// add user from the request and add it to the body of request
ctx.request.body.user = ctx.state.user.id;
// call the function to creating comment with data
let entity = await strapi.services.comment.create(ctx.request.body);
// return data for api after removing field which are not exported
return sanitizeEntity(entity, { model: strapi.models.comment });
},
async update(ctx) {
// get the id of comment which is updated
const { id } = ctx.params;
// finding the comment for user and id
const [comment] = await strapi.services.comment.find({
id: ctx.params.id,
'user.id': ctx.state.user.id,
});
// comment does not exist send error
if (!comment) {
return ctx.unauthorized(`You can't update this entry`);
}
// update the comment
let entity = await strapi.services.comment.update({ id }, ctx.request.body);
// return data for api after removing field which are not exported
return sanitizeEntity(entity, { model: strapi.models.comment });
},
async delete(ctx) {
// get the id of comment which is updated
const { id } = ctx.params;
// finding the comment for user and id
const [comment] = await strapi.services.comment.find({
id: ctx.params.id,
'user.id': ctx.state.user.id,
});
// comment does not exist send error
if (!comment) {
return ctx.unauthorized(`You can't update this entry`);
}
// delete the comment
let entity = await strapi.services.comment.delete({ id });
// return data for api after removing field which are not exported
return sanitizeEntity(entity, { model: strapi.models.comment });
},
};

We just add an extra layer over the function provided by strapi, so that we can add user data to the request body, and rest is handled by strapi. Now we need to change the “Authenticated” user role so that users can create, edit, and delete comments.

We give a user permission to create, delete, edit comments.

Frontend

For the frontend, we will use Gatsbyjs. We would create a new gatsby project using gatsby new frontend. File structure for our project is:

src/
├── components
│ ├── card.js
│ └── dialog.js
├── images
└── pages
├── 404.js
├── blog.js
└── index.js

Different Compoments

  • card.js contains a simple card component that displays information provided to it as props.
  • dialog.js contains a dialog for sign in and signup.
  • blog.js is used to display blogs and comments.
  • index.js is the homepage, it display list of blogs
  • 404.js shows URL not found.

Homepage

We make a GET request to API /blogs to fetch all the blogs. Then it maps over a list of blogs and displays Card component for each blog. It also contains code for displaying login/register dialog. When the user clicks on a Card it navigates them to /blog page.

import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import Card from "../components/card";
import Dialog from "../components/dialog"
import { Button } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
textAlign: "center"
},
paper: {
height: 500,
width: 400,
},
control: {
padding: theme.spacing(2),
},
}));
export default function () {
const classes = useStyles();
const [blogs, setBlogs] = useState([])
const [open, setOpen] = useState(false)
const [login, setLogin] = useState(false)
// fetch all blogs
React.useEffect(() => {
fetch("http://localhost:1337/blogs").then(res => res.json()).then(val => setBlogs(val))
}, [])
return (
<>
{/*dialog for authentication */}
<Dialog open={open} setOpen={setOpen} login={login} />
<Grid container className={classes.root} spacing={2}>
<Grid item xs={12}>
<Grid container justify="center">
<Grid item xs={10}>
<Typography variant="h3" component="h2" gutterBottom gutterLeft>Blogs</Typography>
</Grid>
{/*check if token is present or not */}
{
!localStorage.getItem("token") ? [<Grid item xs={1}>
<Button onClick={() => { setOpen(true); setLogin(true) }}>Login</Button>
</Grid>,
<Grid item xs={1}>
<Button onClick={() => { setOpen(true); setLogin(false) }}> Register</Button>
</Grid>] : ""
}
</Grid>
</Grid>
<Grid item xs={12}>
<Grid container justify="center" spacing={10}>
{/*map through list of blog and create list of cards */}
{blogs.map((value) => (
<Grid key={value} item>
<Card value={value} />
</Grid>
))}
</Grid>
</Grid>
</Grid>
</>
);
}

Card Component

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardMedia from '@material-ui/core/CardMedia';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Collapse from '@material-ui/core/Collapse';
import Typography from '@material-ui/core/Typography';
import { red } from '@material-ui/core/colors';
import { Link } from 'gatsby';
const useStyles = makeStyles((theme) => ({
root: {
maxWidth: 345,
},
media: {
height: 0,
paddingTop: '56.25%', // 16:9
},
expand: {
transform: 'rotate(0deg)',
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest,
}),
},
expandOpen: {
transform: 'rotate(180deg)',
},
avatar: {
backgroundColor: red[500],
},
}));
export default function NewCard({ value }) {
const classes = useStyles();
return (
<Link to={`/blog`} state={{ value }}>
<Card className={classes.root}>
<CardHeader
subheader={`Published On ${new Date(value.created_at).toLocaleDateString("in")}`}
/>
<CardMedia
className={classes.media}
image={"http://localhost:1337" + value.image.url}
/>
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">
{value.title}
</Typography>
</CardContent>
</Card></Link>
);
}

Dialog Component

We make a POST request to /auth/local/register for user signup with a username, email, and password. When a register is successful, a JWT token is returned which is saved in local storage and can be used later. For log in we make a POST request to /auth/local with two fields identifier and password. identifier can be email or username.

import React, { useState } from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
export default function FormDialog({ open, setOpen, login }) {
const [pass, setPass] = useState("")
const [email, setEmail] = useState("")
const [user, setUser] = useState("")
const handleSubmit = () => {
if (!login)
fetch("http://localhost:1337/auth/local/register", {
method: "post",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
password: pass,
email,
username: user
})
}).then((res) => res.json())
.then(res => localStorage.setItem("token", res.jwt)).finally(() => setOpen(false))
else
fetch("http://localhost:1337/auth/local", {
method: "post",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
password: pass,
identifier: user || email
})
}).then((res) => res.json())
.then(res => localStorage.setItem("token", res.jwt)).finally(() => setOpen(false))
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">{login ? "Login" : "Register"}</DialogTitle>
<DialogContent>
<DialogContentText>
Please provide details
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="email"
label="Email Address"
type="email"
fullWidth
value={email}
onChange={(e) => { setEmail(e.target.value) }}
/>
<TextField
autoFocus
margin="dense"
id="username"
label="Username"
type="email"
fullWidth
value={user}
onChange={(e) => { setUser(e.target.value) }}
/>
<TextField
autoFocus
margin="dense"
id="password"
label="Password"
type="password"
fullWidth
value={pass}
onChange={(e) => { setPass(e.target.value) }}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={handleSubmit} color="primary">
Submit
</Button>
</DialogActions>
</Dialog>
</div>
);
}

Blog Page

We will take the details of the blog from the location prop passed to the page. We will fetch comments for the blog using a GET request to /comments?blog={{blog-id}} blog-id is the id of the current blog. We make a POST request to /comments with JWT token in the header. This token is saved in the local storage.

import React, { useState, useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Avatar from '@material-ui/core/Avatar';
import { TextareaAutosize } from '@material-ui/core';
import Button from "@material-ui/core/Button"
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
textAlign: "center"
},
paper: {
height: 500,
width: 400,
},
control: {
padding: theme.spacing(2),
},
content: {
margin: "100px"
}
}));
export default function ({ location }) {
const classes = useStyles();
const [comments, setComments] = useState([])
const [content, setContent] = useState("")
useEffect(() => {
fetch(`http://localhost:1337/comments?blog=${location.state.value.id}`).then(res => res.json()).then(val => setComments(val))
}, [])
const submitComment = () => {
fetch("http://localhost:1337/comments", {
method: "post",
headers: {
"content-type": "application/json",
authorization: `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify({
content,
blog: location.state.value.id
})
}).then(() => fetch(`http://localhost:1337/comments?blog=${location.state.value.id}`).then(res => res.json()).then(val => setComments(val)))
}
return (
<>
<Grid container className={classes.root} spacing={2}>
<Grid item xs={12}>
<Grid container justify="center">
<Grid item xs={10}>
<Typography variant="h3" component="h2" gutterBottom gutterLeft>{location.state.value.title}</Typography>
</Grid>
</Grid>
</Grid>
<Grid container justify="center">
<img src={"http://localhost:1337" + location.state.value.image.url}></img>
</Grid>
<Grid item xs={12} className={classes.content}>
<Grid container justify="center" spacing={10}>
{location.state.value.content}
</Grid>
</Grid>
<Typography variant="h4" component="h2" gutterBottom gutterLeft>Comments</Typography>
<Grid item xs={12}><TextareaAutosize minLength={10} rowsMin={10} style={{ width: "100%" }} value={content} onChange={(e) => setContent(e.target.value)} /></Grid>
<Grid item xs={12}><Button onClick={submitComment}>Submit comment</Button></Grid>
<Grid item xs={12}>
<Grid container justify="left">
<List>
{
comments.map((val) => <ListItem>
<ListItemIcon><Avatar>{val.user.username[0]}</Avatar></ListItemIcon>
<ListItemText primary={`${val.user.username} said `} />
<ListItemText secondary={": " + val.content} />
</ListItem>)
}
</List>
</Grid>
</Grid>
</Grid>
</>
);
}

Database

Strapi support both NoSQL and SQL database. Changing the database is as simple as changing the env variable in the configuration folder. By default, Strapi uses SQLite which is good for local testing but in production one should use a production-ready database like PostgresSQL or MySQL. We will use PostgreSQL. For changing the database we will edit config/environments/production/database.json file.

{
"defaultConnection": "default",
"connections": {
"default": {
"connector": "bookshelf",
"settings": {
"client": "postgres",
"host": "${process.env.DATABASE_HOST }",
"port": "${process.env.DATABASE_PORT }",
"database": "${process.env.DATABASE_NAME }",
"username": "${process.env.DATABASE_USERNAME }",
"password": "${process.env.DATABASE_PASSWORD }"
},
"options": {}
}
}
}

Now it will pick database credentials from the environment variable in production.

Conclusion

We have gone through some of basic of Strapi. Strapi is great for creating a backend API. It is very customizable and supports a wide array of integration. Strapi can be used with Nuxtjs, Reactjs, Angular any frontend framework. We learned how to create database schema with relations, authentication, customizing controllers, and filtering data.