Hello wonderful people of DEV! Thanks for 50 followers by the way, you're all awesome! I'm back with another tutorial for you guys, and today, I've got Strapi!
Strapi is a CMS (Content Management System), like Wordpress, except, Strapi is headless, meaning it has a bunch of APIs that allow you to use its features without needing to be restricted to its frontend. Basically, Strapi is a BaaS that we host ourselves. We don't have to write any backend code to use Strapi. All we have to do, is install it on our machine.
So, today, I'll show you how you can make an Instagram clone using Strapi as the backend and any framework you want (I've gone with Svelte) as the frontend. You can checkout the live app [here]. The source code for the project is available [here].
Creating a Strapi project
If you remember, in my last tutorial, when I showed you serverless with Firebase, we had to sign up to Firebase, create a project, create an app, and finally add the Firebase config to our project.
This time, it is much simpler. You will need NodeJS and NPM installed, which I hope you all do already. To create a strapi app, we execute this command:
# cms is the name of the folder that strapi will be in
npx create-strapi-app cms --quickstart
cd cms
npm run develop
Reminds you of
create-react-app
, doesn't it?
This will create a Strapi app in the cms
folder, go to that folder and start the app. You can leave this terminal window running in the background.
The
--quickstart
option makes it easier for us to get the app running by using SQLite as our database.
Creating our frontend application
Let's now focus on the frontend. I'm gonna use Svelte for the frontend, since I've fallen in love with it. Svelte is easy to understand, so you can translate it to the framework you're using. I'm also going to be using typescript as the language, instead of javascript, so be careful, and remove any type assertions or interfaces and stuff like that if you'll be using javascript.
To create the svelte app, we can use degit.
npx degit sveltejs/template frontend
cd frontend
# Open in VSCode
code .
# Make app into typescript
node scripts/setupTypescript.js
npm i
While we're setting up the frontend, let's also add a router like Page.js to make an SPA app.
npm i page
# for typescript only
npm i -D @types/page
Basic boilerplate for the frontend
Now, let's add the basic boilerplate. Stuff like adding styles, the frontpage UI, and adding the router.
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Quickstagram - Instagram, but quick!</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link href="https://use.fontawesome.com/releases/v5.0.1/css/all.css" rel="stylesheet">
<link rel='stylesheet' href='/build/bundle.css'>
<script defer src='/build/bundle.js'></script>
</head>
<body>
</body>
</html>
<!-- src/App.svelte -->
<script lang="ts">
import { setContext } from "svelte";
import router from "page";
import { parse } from "qs";
import Index from "./routes/index.svelte";
import Navbar from "./components/Navbar.svelte";
export let strapiApiUrl: string;
let page;
let params;
let queryString;
function setupRouteParams(ctx: PageJS.Context, next) {
params = ctx.params;
queryString = parse(ctx.querystring);
next();
}
router("/", setupRouteParams, () => (page = Index));
router.start();
setContext("apiUrl", strapiApiUrl);
</script>
<Navbar />
<!-- This component just renders the component `this`. It is used to render components dynamically, like how we're doing -->
<svelte:component this={page} {params} {queryString} />
<!-- src/components/Index.svelte -->
<script lang="ts">
import Auth from "../components/Auth.svelte";
export const queryString = {};
export const params = {};
</script>
<div class="w3-container">
<h1 class="w3-center w3-xxxlarge">Quickstagram</h1>
<p class="w3-center w3-large w3-text-gray">Instagram, but quicker!</p>
<div class="w3-center">
<a
href="/auth?action=register"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">Register</a>
<a
href="/auth?action=login"
class="w3-button w3-white w3-border w3-border-black w3-hover-white">Login</a>
</div>
<Auth />
</div>
<!-- src/components/Navbar.svelte -->
<script lang="ts">
import { slide } from "svelte/transition";
let active = false;
</script>
<style>
.toggler {
display: none;
}
@media (max-width: 600px) {
.logo {
display: block;
width: 100%;
}
.logo .toggler {
float: right;
display: initial;
}
.nav {
display: flex;
width: 100%;
flex-direction: column;
}
.nav a {
text-align: left;
}
}
</style>
<div class="w3-bar w3-blue">
<div class="logo">
<a
href="/"
class="w3-bar-item w3-text-white w3-button w3-hover-blue">Quickstagram</a>
<button
class="toggler w3-button w3-blue w3-hover-blue"
on:click={() => (active = !active)}>
<i class="fas fa-{active ? 'times' : 'bars'}" /></button>
</div>
<div class="w3-right w3-hide-small">
<a href="/upload" class="w3-bar-item w3-button w3-hover-blue">Upload</a>
<a
href="/auth?action=login"
class="w3-bar-item w3-button w3-hover-blue">Login</a>
<a
href="/auth?action=register"
class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
</div>
{#if active}
<div class="w3-right nav w3-hide-large w3-hide-medium" transition:slide>
<a
href="/upload"
class="w3-bar-item w3-button w3-hover-blue">Upload</a>
<a
href="/auth?action=login"
class="w3-bar-item w3-button w3-hover-blue">Login</a>
<a
href="/auth?action=register"
class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
</div>
{/if}
</div>
<!-- src/components/ErrorAlert.svelte -->
<script lang="ts">
export let message: string;
</script>
<div
class="w3-panel w3-pale-red w3-leftbar w3-border-red w3-text-red w3-padding">
{message}
</div>
src/components/Auth.svelte
<script lang="ts">
import Error from "./ErrorAlert.svelte";
import { fade } from "svelte/transition";
type AuthMode = "login" | "register";
export let authMode: AuthMode = "register";
let loginError: string | null = null;
let registerError: string | null = null;
let email = "";
let password = "";
let cpassword = "";
let username = "";
function login() {
email = email.trim();
password = password.trim();
if (!email || !password) {
loginError = "Fill out all fields!";
return;
}
loginError = null;
}
function register() {
email = email.trim();
password = password.trim();
cpassword = cpassword.trim();
username = username.trim();
if (!email || !password || !cpassword || !username) {
registerError = "Fill out all fields!";
return;
}
registerError = null;
}
</script>
<style>
.auth-box {
width: 40%;
margin: 1rem auto;
}
@media (max-width: 600px) {
.auth-box {
width: 80%;
}
}
</style>
<div class="w3-container">
<div class="w3-card-4 w3-border w3-border-black auth-box">
<div class="w3-bar w3-border-bottom w3-border-gray">
<button
style="width: 50%"
on:click={() => (authMode = 'login')}
class="w3-bar-item w3-button w3-{authMode === 'login' ? 'blue' : 'white'} w3-hover-{authMode === 'login' ? 'blue' : 'light-gray'}">Login</button>
<button
style="width: 50%"
on:click={() => (authMode = 'register')}
class="w3-bar-item w3-button w3-{authMode === 'register' ? 'blue' : 'white'} w3-hover-{authMode === 'register' ? 'blue' : 'light-gray'}">Register</button>
</div>
<div class="w3-container">
<h3>{authMode === 'login' ? 'Login' : 'Register'}</h3>
{#if authMode === 'login'}
<form on:submit|preventDefault={login} in:fade>
{#if loginError}
<Error message={loginError} />
{/if}
<div class="w3-section">
<label for="email">Email</label>
<input
type="email"
bind:value={email}
placeholder="Enter your email"
id="email"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="password">Password</label>
<input
type="password"
bind:value={password}
placeholder="Enter your password"
id="password"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Login</button>
<button
class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
on:click={() => (authMode = 'register')}>Register</button>
</div>
</form>
{:else}
<form on:submit|preventDefault={register} in:fade>
{#if registerError}
<Error message={registerError} />
{/if}
<div class="w3-section">
<label for="username">Username</label>
<input
type="text"
bind:value={username}
placeholder="Enter a username"
id="username"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="email">Email</label>
<input
type="email"
bind:value={email}
placeholder="Enter your email"
id="email"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="password">Password</label>
<input
type="password"
bind:value={password}
placeholder="Enter a password"
id="password"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="cpassword">Confirm Password</label>
<input
type="password"
bind:value={cpassword}
placeholder="Re-enter that password"
id="cpassword"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Register</button>
<button
class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
on:click={() => (authMode = 'login')}>Login</button>
</div>
</form>
{/if}
</div>
</div>
</div>
Now, let's also add a /auth
route that just renders our auth component.
<!-- src/routes/auth.svelte -->
<script lang="ts">
import Auth from "../components/Auth.svelte";
import router from "page";
export const params = {};
export let queryString: { action: "login" | "register"; next: string };
</script>
<Auth authMode={queryString.action} on:auth={() => router.redirect(queryString.next)} />
We need to register this auth.svelte
component in our router, so let's do just that:
<!-- src/App.svelte -->
<script lang="ts">
// ...
import Auth from "./routes/auth.svelte";
// ...
router("/", setupRouteParams, () => (page = Index));
router("/auth", setupRouteParams, () => (page = Auth));
// ...
</script>
<!-- ... -->
You may notice that the routes don't work. This is because our app isn't configured to be SPA compatible yet. Let's do so. Edit your package.json
:
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public -s --host",
"validate": "svelte-check"
}
Rerun your app with npm run dev
.
Configuring strapi
We need to create models for our database since we're using an SQL database. Fortunately, Strapi makes it easy. Head over to the Strapi admin panel at localhost:1337/admin.
Make sure your server is still running! If not, run it with:
npm run develop
Creating a post modal
Let's define our Post, i.e. what should a post contain. Since this is an Instagram clone, we can ask "What does an Instagram post contain?" It contains:
- The image of the post
- The post's author
- The post's text
- The post's likes
- The post's comments
- When the post was created
Let's create a post
collection in Strapi. Follow the steps in the video below:
In the video above, I created a
created
column. This is not required, because Strapi does it automatically at that time.
Now, we need comments. Let's create a comment
collection.
We can also set certain properties to fields (called columns in SQL).
Now, for the selling point of SQL, relations. Let's add special columns called relations that allow us to reference another table using that field.
And we're done! Let's now access the Strapi API!
Accessing the API
We need a program to make API requests like Insomnia or Postman (I'm gonna go with the former). If you're using a *nix system like MacOS/Linux, you can also use the cURL command.
Unauthenticated requests
Anybody should be able to access parts of our API without logging in, for example, they can access the Posts, Images and comments, but should not be able to delete or upload them. Let's try accessing the posts. To access any collection from the API, the endpoint will be:
GET http://localhost:1337/<collection_name>
To get the collection name for any endpoint:
Great! Now that we have the collection name, let's call the endpoint http://localhost:1337/posts
What! We get a 403 FORBIDDEN
error. Why so? This is because we haven't set up any rules yet. Rules determine who gets to see what. Let's change the rules for post
and comment
to this:
Now, if we rerun the API request:
We can see that it works!
Same thing in cURL:
$ curl -X GET http://localhost:1337/posts
[]
We can also do the same thing with comments:
Authenticated requests
Now, let's focus on users who are authenticated. An authenticated user should be able to do the same things as an unauthenticated user, but, they can also create posts and comments. If we try creating a comment by sending a POST
request to http://localhost:1337/comments
, we get a 403 FORBIDDEN
error.
Remember to set
Content-Type
header toapplication/json
!
Same thing with cURL
$ curl -X POST -H "Content-Type: application/json" -d '{"content": "Hello world!"}' http://localhost:1337/comments
{"statusCode":403,"error":"Forbidden","message":"Forbidden"}
Let's fix that by authenticating. When we authenticate, we get a JSON Web Token back that we can then attach to other requests using the Authorization header. To authenticate, we need to send a POST
request to http://localhost:1337/auth/local
. This POST
request should contain an identifier
field, which can either be the email or username of the user, and a password
field, which is the user's password.
We don't have any users. Let's register one:
You can see that we got back a token
. This token
is the JSON Web Token I was talking about earlier. Let's use this in our request from earlier to create a new post:
$ curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer paste_your_token_here" -d '{"content": "Hello, world!"}' http://localhost:1337/comments
{"id":1,"content":"Hello, world!","user":null,"post":null,"published_at":"2020-11-11T07:07:26.552Z","created_at":"2020-11-11T07:07:26.564Z","updated_at":"2020-11-11T07:07:26.564Z"}
The comment has been successfully added! Congratulations! Let's look at the Strapi Admin Panel. We can see that our changes have been reflected!
If your token expires, you can log in again by sending a >
POST
request to/auth/local
. Eg:curl -X POST -H "Content-Type: application/json" -d '{"identifier": "your email", "password": "your password"}' http://localhost:1337/auth/local
Conclusion
That was a look at strapi.js. This is the first part of this series. In the next part, we'll get to the frontend and other juicy stuff!
Top comments (11)
I really liked your tutorial!
Just want to add a minor typo in the example which causes an error while running the Svelte frontend app. In the src/App.svelte the import should be like:
While the example says:
And maybe it's good to specify that Strapi uses at least node version 14 +. :)
So that's it! Everything else works fine! Thanks for the tips!
Oops! I'll change it right away! Thank you
If you're talking about the resolution, I tried using 720p, because my laptop is 1440x900 (macbook air), so I couldn't do 1080p
If you're talking about the text size, I'm sorry, I'll try to zoom in further next time :)
Awesome article, thank you! I work for Strapi and would love to chat about a potential talk on this topic at StrapiConf. Can we chat :) ?
I've never been on any show like this before! I can provide my discord arnu515#9699, friend me there to talk :)
I loved the article!
I'm curious to see the end result ^^.
Thank you very much! I'm working on the second part of the article right now! Hopefully it comes out soon!
Thanks! Will try
Hello, nice article, I've got error: Uncaught ReferenceError: qs is not defined - main.ts. I saw your github, there's also defined qs.
thanks
Hmm, looks like you have to import it.
If you get an error after this also, (maybe module not found), then just install it:
If using typescript, install types too:
The second part is out! dev.to/arnu515/build-an-instagram-...