In this post, we will learn how to use three modern and powerful technologies Strapi, Svelte, and Tailwind CSS, to build an elegant and functional todo app. We will use Strapi for our backend, Svelte as our JavaScript framework for building our interface, and Tailwind CSS for styling our App.
Below is a demo of what we will be building with these three technologies.
Before we get started, let's see what the technologies we'll be working with is about:
What is Strapi?
Strapi is an open-source headless Node.js CMS built purely with JavaScript that helps to create fully customizable and scalable APIs. With Strapi, we can build APIs using REST or GraphQL architecture.
What is Svelte?
Svelte is another core technology we will be using in the tutorial. Svelte is the talk of the town in the JavaScript ecosystem because it follows an entirely different approach in building user interfaces as opposed to React and Vue, as it doesn't use the Virtual DOM.
What is Tailwind CSS?
Tailwind CSS is a utility-first CSS framework use for writing CSS in a composable fashion right in your HTML. It is a powerful utility that allows you to build modern, pixel-perfect, and fully responsive interfaces without writing your custom CSS. Unlike Bootstrap, Tailwind CSS is not opinionated, and it doesn't have prebuilt components, making building good looking-interfaces fun!
In the tutorial, we will build REST API using Strapi and consume our data from the client-side using Axios and make our components with Svelte.
We will write our JavaScript functions perform CRUD operation on our todos data coming from our backend powered by Strapi, and style our App, so it looks elegant with Tailwind CSS.
Download and install the following technologies:
1 npm i yarn -g
No Knowledge of Svelte, Strapi, or Tailwind CSS is required. However, it will help if you have some familiarity with CSS and JavaScript.
Now, let's bootstrap our Strapi project. To do this, navigate to the folder where you want your project to be and run one of the following commands in your terminal:
1 npx create-strapi-app strapi-todo-api --quickstart
2 #or
3 yarn create strapi-app strapi-todo-api --quickstart
This command will create a Strapi project called "strapi-todo-api" with all the necessary dependencies installed.
Once that is done, cd
into the project directory and run one of the following commands:
1 npm run develop
2 #or
3 yarn develop
This command will start our dev server and open our application on our default browser, and you should see this on the page:
Fill in your details accordingly and click on the let's start button and you'll be redirected to the onboarding page:
Next, let’s create our content to populate our todo items
Now, let's build the collection for our data and fill in some todo items to display when we fetch our data in our Svelte App. To achieve this, click on Collection Type Builder on the left side of the admin panel, and after that, click on create new collection type and fill in the following data in the "Display name" field as shown below. When that is done, click on the click on continue.
This image will create a new Todo
collection so let's go ahead and add documents to our newly created collections.
We will add two fields in each of the documents in our Todo
collection, one for our todo items and the other a boolean to check the state of our todo; whether it is marked as done.
Click on the "Add another field" button and add a field with type text and check the long text type as shown below:
Click on add another field and select the boolean type. We will call our field isCompleted
. We will be using it to toggle our todo state when a user checks on the completed checkbox in our App.
The next thing we have to do is to set up our roles and permission so that we can get access to our data from our Svelte App. To do this, navigate to Setting
→ Users & Permissions Plugin
→ and then tick on the select all checkbox under the Permissions
section and click on the Save
button:
With that done, let's test our API with Postman. To access our resource, open [http://localhost:1337/todos](http://localhost:1337/todos)
in your browser or using an API client like Postman to test our created data.
Now that we are creating our API using Strapi let's move over to the client-side of our application, where we build out our components using Svelte. To generate a new Svelte app, run the following command in your terminal to generate a project in a folder called "SvelteTodoApp":
1 npx degit sveltejs/template SvelteTodoApp
2 cd SvelteTodoApp
3 npm install
4 npm run dev
When this is done, navigate to [http://localhost:54890](http://localhost:54890)
from your browser, and you should see this:
You have to install the Svelte extension to get syntax highlighting and IntelliSense support for VS Code
Create a file in src/Todo.svelte
and replace everything in the main
tag in App.svelte
with the following:
1 <Todo />
Next, let's add Tailwind CSS to our Svelte App. Run one of the following commands in your terminal to install the necessary dependencies:
1 npm install tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
2 # or
3 yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
4 # and
5 npx tailwindcss init tailwind.config.js
This will create a tailwind.config.js
and rollup.config.js
file in the root of our app. Replace the code in your tailwind.config.js
file with the following:
1 const production = !process.env.ROLLUP_WATCH;
2 module.exports = {
3 purge: {
4 content: ['./src/**/*.svelte'],
5 enabled: production,
6 },
7 darkMode: false,
8 theme: {
9 extend: {},
10 },
11 variants: {
12 extend: {},
13 },
14 plugins: [],
15 future: {
16 purgeLayersByDefault: true,
17 removeDeprecatedGapUtilities: true,
18 },
19 };
Next, open your rollup.config.js
and replace what you have with the following:
1 import svelte from 'rollup-plugin-svelte';
2 import commonjs from '@rollup/plugin-commonjs';
3 import resolve from '@rollup/plugin-node-resolve';
4 import livereload from 'rollup-plugin-livereload';
5 import { terser } from 'rollup-plugin-terser';
6 import css from 'rollup-plugin-css-only';
7 import sveltePreprocess from 'svelte-preprocess';
8 const production = !process.env.ROLLUP_WATCH;
9 function serve() {
10 let server;
11 function toExit() {
12 if (server) server.kill(0);
13 }
14 return {
15 writeBundle() {
16 if (server) return;
17 server = require('child_process').spawn(
18 'npm',
19 ['run', 'start', '--', '--dev'],
20 {
21 stdio: ['ignore', 'inherit', 'inherit'],
22 shell: true,
23 }
24 );
25 process.on('SIGTERM', toExit);
26 process.on('exit', toExit);
27 },
28 };
29 }
30 export default {
31 input: 'src/main.js',
32 output: {
33 sourcemap: true,
34 format: 'iife',
35 name: 'app',
36 file: 'public/build/bundle.js',
37 },
38 plugins: [
39 svelte({
40 preprocess: sveltePreprocess({
41 sourceMap: !production,
42 postcss: {
43 plugins: [require('tailwindcss'), require('autoprefixer')],
44 },
45 }),
46 compilerOptions: {
47 // enable run-time checks when not in production
48 dev: !production,
49 },
50 }),
51 // we'll extract any component CSS out into
52 // a separate file - better for performance
53 css({ output: 'bundle.css' }),
54 // If you have external dependencies installed from
55 // npm, you'll most likely need these plugins. In
56 // some cases you'll need additional configuration -
57 // consult the documentation for details:
58 // https://github.com/rollup/plugins/tree/master/packages/commonjs
59 resolve({
60 browser: true,
61 dedupe: ['svelte'],
62 }),
63 commonjs(),
64 // In dev mode, call `npm run start` once
65 // the bundle has been generated
66 !production && serve(),
67 // Watch the `public` directory and refresh the
68 // browser on changes when not in production
69 !production && livereload('public'),
70 // If we're building for production (npm run build
71 // instead of npm run dev), minify
72 production && terser(),
73 ],
74 watch: {
75 clearScreen: false,
76 },
77 };
Finally, add the following to your App.svelte
:
1 <style global lang="postcss">
2 @tailwind base;
3 @tailwind components;
4 @tailwind utilities;
5 </style>
We will use Axios to make our HTTP request to get our data from the backend. Run the following command in your Svelte project directory to install Axios:
1 npm install axios
2 #or
3 yarn add axios
When that is done, add the following to the top of your script section in your Svelte App:
1 import { onMount } from 'svelte';
2 import axios from 'axios';
3
4 let isError = null;
5 let todos = [];
The [onMount](https://svelte.dev/docs#onMount)
is a lifecycle function in Svelte that schedules a callback to run immediately after the component is mounted to the DOM. The todos
array is where all the data we get from our API will be.
Let’s create a function called getTodo
and call it on the onMount
lifecycle function:
1 const getTodos = async () => {
2 try {
3 const res = await axios.get('http://localhost:1337/todos');
4 todos = res.data;
5 } catch (e) {
6 isError = e;
7 }
8 };
9
10 onMount(() => {
11 getTodos();
12 });
The getTodos
function will make an asynchronous call to the endpoint we created earlier and set the result of our todos array to be the data from our API.
To render the output of our todos in the DOM, add the following block of code in your template:
1 // Todo.svelte
2 {#if todos.length > 0}
3 <p class="text-2xl mb-4">Today's Goal</p>
4 {/if}
5 {#if isError}
6 <p class="text-xl mb-2 text-red-600">{isError}</p>
7 {/if}
8 <ul>
9 {#each todos as todo}
10 <li
11 class="rounded-xl bg-black bg-opacity-10 p-5 mb-4 flex items-center justify-between cursor-pointer hover:shadow-lg transition transform hover:scale-110"
12 >
13 <div class="flex items-center w-full">
14 <input
15 type="checkbox"
16 class="mr-3"
17 bind:checked={todo.isCompleted}
18 on:click={toggleComplete(todo)}
19 />
20 <input
21 class:completed={todo.isCompleted}
22 class="border-0 bg-transparent w-full"
23 bind:value={todo.todoItem}
24 on:change={updateTodo(todo)}
25 on:input={updateTodo(todo)}
26 />
27 </div>
28 <button on:click={deleteTodo(todo)} class="border-0"
29 ><img src={deletIcon} alt="delete todo" class="w-6 " /></button
30 >
31 </li>
32 {:else}
33 <p>No goals for today!</p>
34 {/each}
35 </ul>
Notice how we are making use of the {#if expression} to check if we have any items in our todos
array and then conditionally a text, We are also doing the same thing with the isError
checking if there’s an error from our API.
The {#each ...} expression is where the magic happens, we are looping through our array of todos and rendering the todoItem
and then we use the {:``[else](https://svelte.dev/docs#each)``}...{/each}
to conditionally render a text when there’s no result.
Notice we haven’t created the
updateTodo
anddeleteTodo
function. We will do that later
Next, we want to create a function that will allow users to add a todo to your API from our Svelte App. To do this, add the following block of code in the script section of your App:
1 let todoItem = '';
2
3 const addTodo = async () => {
4 try {
5 if (!todoItem) return alert('please add a goal for today!');
6 const res = await axios.post('http://localhost:1337/todos', {
7 todoItem,
8 isCompleted: false,
9 });
10 // Using a more idiomatic solution
11 todos = [...todos, res?.data];
12 todoItem = '';
13 } catch (e) {
14 isError = e;
15 }
The todoItem
variable is what we will use to get the user's input, and then in our addTodo
function, we are making sure it's not empty before making a POST request to our todos endpoint.
Next, we send the user input stored in todoItem
and setting isCompleted: false
because we want the todo to be undone when created. Finally, we are updating our todos array with the data coming in from the API call.
Add the following markup to after the else statement:
1 <input
2 type="text"
3 bind:value={todoItem}
4 class="w-full rounded-xl bg-white border-0 outline-none bg-opacity-10 p-4 shadow-lg mt-4"
5 placeholder="Add new goals"
6 />
7
8 <button
9 on:click={addTodo}
10 class="my-5 p-5 bg-black text-white rounded-xl w-full hover:bg-opacity-60 transition border-0 capitalize flex items-center justify-center"
11 ><span><img src={addIcon} alt="add todo" class="w-6 mr-4" /></span>Add new
12 todo</button>
Notice the bind:value={todoItem}
in our input field above. This binding is used to achieve two-way data binding in Svelte.
Updating our App will happen in two forms. Users can mark todo as done by clicking on the checkbox beside each todo item and editing the todo text.
Let’s create a function for toggling the checkbox:
1 const toggleComplete = async (todo) => {
2 const todoIndex = todos.indexOf(todo);
3 try {
4 const { data } = await axios.put(
5 `http://localhost:1337/todos/${todo.id}`,
6 {
7 isCompleted: !todo.isCompleted,
8 }
9 );
10 todos[todoIndex].isCompleted = data.isCompleted;
11 } catch (e) {
12 isError = e;
13 }
14 };
We are getting the index of the todo item that is clicked using the indexOf
method and then making a PUT request to the server to update the particular todo item.
We are toggling the isCompleted
field in our API by sending isCompleted: !todo.isCompleted
in our request. When our API is resolved, we update our todos array in our state with the payload from our API by setting todos[todoIndex].isCompleted = data.isCompleted
;
Next, let’s create a function to edit the todo text:
1 const updateTodo = async (todo) => {
2 const todoIndex = todos.indexOf(todo);
3 try {
4 const { data } = await axios.put(
5 `http://localhost:1337/todos/${todo.id}`,
6 {
7 todoItem: todo.todoItem,
8 }
9 );
10 todos[todoIndex].todoItem = data.todoItem;
11 } catch (e) {
12 isError = e;
13 }
14 };
Our updateTodo
function does almost the same thing as the toggleComplete
except that it updates the todo text.
After that is done, add the following to your template:
1 <div class="flex items-center w-full">
2 <input
3 type="checkbox"
4 class="mr-3"
5 bind:checked={todo.isCompleted}
6 on:click={toggleComplete(todo)}
7 />
8 <input
9 class:completed={todo.isCompleted}
10 class="border-0 bg-transparent w-full"
11 bind:value={todo.todoItem}
12 on:change={updateTodo(todo)}
13 on:input={updateTodo(todo)}
14 />
15 </div>
To sync the data from our state to our input field, we are using the [bind:value={}](https://svelte.dev/docs#bind_element_property)
syntax provided to us by Svelte.
Observe how we are binding a class attribute in our input field using this syntax: class:completed={todo.isCompleted}
. We are telling Svelte that it should add the completed
class whenever todo.isCompleted
is truthy.
This will apply the following class:
1 <style>
2 .completed {
3 text-decoration: line-through;
4 }
5 </style>
Next, let’s create a function to delete items from our API and todos
array:
1 const deleteTodo = async (todo) => {
2 try {
3 await axios.delete(`http://localhost:1337/todos/${todo.id}`);
4 todos = todos.filter((to) => to.id !== todo.id);
5 } catch (e) {
6 isError = e;
7 }
Notice we call the delete
method on Axios
and then appending the id
value of the todo item the user clicks to our URL. This call will effectively remove the item clicked from our todos
collection in our API, and then we are filtering our todos
array and returning all the todos except for the one deleted.
This is how we use the function in our template:
1 <button on:click={deleteTodo(todo)} class="border-0"><img src={deletIcon} alt="delete todo" class="w-6 " /></button>
In this article, we've seen how powerful and very easy to use Strapi is. Setting up a backend project is like a walk in the park, very simple and easy. By just creating our collections, Strapi will provide us with endpoints we need following best web practices.
We've also seen our to work with Svelte, and we built our component using one and styled our App with Tailwind CSS.
You can find the complete code used in this tutorial for the Svelte App here, and the backend code is available on GitHub. You can also find me on Twitter, LinkedIn, and GitHub. Thank you for reading! Feel free to drop a comment to let me know what you thought of this article.
I love to call myself a JavaScript Developer with industry experience in building scalable and performant applications that run on the web and your smartphone with cutting-edge technology. I author meaningful technical content, regularly.