How to Build a To-do app with Svelte, Strapi & Tailwind CSS
Tags: svelte, tailwindcss, tutorial, todayilearned
Published: 2021-07-22 13:57:04

## Introduction

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.


![To-do app with Svelte, Strapi and Tailwind CSS](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625245003576_ezgif.com-gif-maker.gif)


Before we get started, let's see what the technologies we'll be working with is about:

**What is Strapi?**

[Strapi](https://strapi.io/) 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](https://svelte.dev/) 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](https://svelte.dev/blog/virtual-dom-is-pure-overhead).

**What is Tailwind CSS?**

[Tailwind CSS](https://tailwindcss.com/) 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](https://getbootstrap.com/), Tailwind CSS is not opinionated, and it doesn't have prebuilt components, making building good looking-interfaces fun!

## Goals

In the tutorial, we will build REST API using Strapi and consume our data from the client-side using [Axios](https://github.com/axios/axios) and make our components with [Svelte](https://svelte.dev/). 

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](https://tailwindcss.com/).

## Requirement

Download and install the following technologies:

1. [Node.js](https://nodejs.org/) installed on your computer; only LTS versions are supported (version 12.x minimum).
2. [npm](https://docs.npmjs.com/cli/v6/commands/npm-install): Npm ships with Node.js
3. [Yarn](https://yarnpkg.com/): A fast, secure, and reliable Node package manager. To install yarn globally, run the following command on your terminal.

```
    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.


## Creating a Strapi Project

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:

```
    npx create-strapi-app strapi-todo-api --quickstart
    #or
    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.


![Generating a Strapi Project](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625241604185_Screen+Shot+2021-06-30+at+7.29.43+PM.png)


Once that is done, `cd` into the project directory and run one of the following commands:

```
    npm run develop
    #or 
    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:


![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625241937627_Screen+Shot+2021-07-02+at+5.04.53+PM.png)


Fill in your details accordingly and click on the let's start button and you'll be redirected to the onboarding page:


![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625242258475_Screen+Shot+2021-07-02+at+5.10.43+PM.png)


Next, let’s create our content to populate our todo items

## Creating Content Types

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.


![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625243000280_Screen+Shot+2021-07-02+at+5.22.59+PM.png)


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:


![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625245316435_Screen+Shot+2021-07-02+at+6.01.34+PM.png)


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.


![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625245593664_Screen+Shot+2021-07-02+at+6.05.56+PM.png)



## Setting up Roles and Permissions

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:


![Frame 83.png](/uploads/Frame_83_0aa3a7e212.png)


## Testing our API

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](https://www.postman.com/) to test our created data.

![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625267866429_Screen+Shot+2021-07-03+at+12.17.16+AM.png)

![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625267975495_Screen+Shot+2021-07-03+at+12.19.20+AM.png)

## Bootstrap a Svelte Project

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":

```
    npx degit sveltejs/template SvelteTodoApp
    cd SvelteTodoApp
    npm install
    npm run dev
```

When this is done, navigate to `[http://localhost:54890](http://localhost:54890)` from your browser, and you should see this:


![](https://paper-attachments.dropbox.com/s_6BB0FD57EB381C56610A88151E42107F9C2B9079B23473D2B16829E5BD1A9832_1625269788564_Screen+Shot+2021-07-03+at+12.48.49+AM.png)


You have to install the [Svelte extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) 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:

```
      <Todo />
```

## Adding Tailwind CSS to our Svelte App

Next, let's add Tailwind CSS to our Svelte App. Run one of the following commands in your terminal to install the necessary dependencies:

```
    npm install tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
    # or
    yarn add tailwindcss@npm:@tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
    # and
    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:

```
    const production = !process.env.ROLLUP_WATCH;
    module.exports = {
      purge: {
        content: ['./src/**/*.svelte'],
        enabled: production, 
      },
      darkMode: false, 
      theme: {
        extend: {},
      },
      variants: {
        extend: {},
      },
      plugins: [],
      future: {
        purgeLayersByDefault: true,
        removeDeprecatedGapUtilities: true,
      },
    };
```
    

Next, open your `rollup.config.js` and replace what you have with the following:

```
    import svelte from 'rollup-plugin-svelte';
    import commonjs from '@rollup/plugin-commonjs';
    import resolve from '@rollup/plugin-node-resolve';
    import livereload from 'rollup-plugin-livereload';
    import { terser } from 'rollup-plugin-terser';
    import css from 'rollup-plugin-css-only';
    import sveltePreprocess from 'svelte-preprocess';
    const production = !process.env.ROLLUP_WATCH;
    function serve() {
      let server;
      function toExit() {
        if (server) server.kill(0);
      }
      return {
        writeBundle() {
          if (server) return;
          server = require('child_process').spawn(
            'npm',
            ['run', 'start', '--', '--dev'],
            {
              stdio: ['ignore', 'inherit', 'inherit'],
              shell: true,
            }
          );
          process.on('SIGTERM', toExit);
          process.on('exit', toExit);
        },
      };
    }
    export default {
      input: 'src/main.js',
      output: {
        sourcemap: true,
        format: 'iife',
        name: 'app',
        file: 'public/build/bundle.js',
      },
      plugins: [
        svelte({
          preprocess: sveltePreprocess({
            sourceMap: !production,
            postcss: {
              plugins: [require('tailwindcss'), require('autoprefixer')],
            },
          }),
          compilerOptions: {
            // enable run-time checks when not in production
            dev: !production,
          },
        }),
        // we'll extract any component CSS out into
        // a separate file - better for performance
        css({ output: 'bundle.css' }),
        // If you have external dependencies installed from
        // npm, you'll most likely need these plugins. In
        // some cases you'll need additional configuration -
        // consult the documentation for details:
        // https://github.com/rollup/plugins/tree/master/packages/commonjs
        resolve({
          browser: true,
          dedupe: ['svelte'],
        }),
        commonjs(),
        // In dev mode, call `npm run start` once
        // the bundle has been generated
        !production && serve(),
        // Watch the `public` directory and refresh the
        // browser on changes when not in production
        !production && livereload('public'),
        // If we're building for production (npm run build
        // instead of npm run dev), minify
        production && terser(),
      ],
      watch: {
        clearScreen: false,
      },
    };
```


Finally, add the following to your `App.svelte`:

```
    <style global lang="postcss">
      @tailwind base;
      @tailwind components;
      @tailwind utilities;
    </style>
```

## Fetch todo items

We will use [Axios](https://github.com/axios/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:

```
    npm install axios
    #or
    yarn add axios
```


When that is done, add the following to the top of your script section in your Svelte App:

```
    import { onMount } from 'svelte';
    import axios from 'axios';
    
    let isError = null;
    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:

```
    const getTodos = async () => {
        try {
          const res = await axios.get('http://localhost:1337/todos');
          todos = res.data;
        } catch (e) {
          isError = e;
        }
      };
    
    onMount(() => {
        getTodos();
    });
```

    

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:

```
    // Todo.svelte
    {#if todos.length > 0}
        <p class="text-2xl mb-4">Today's Goal</p>
      {/if}
      {#if isError}
        <p class="text-xl mb-2 text-red-600">{isError}</p>
      {/if}
      <ul>
        {#each todos as todo}
          <li
            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"
          >
            <div class="flex items-center w-full">
              <input
                type="checkbox"
                class="mr-3"
                bind:checked={todo.isCompleted}
                on:click={toggleComplete(todo)}
              />
              <input
                class:completed={todo.isCompleted}
                class="border-0 bg-transparent w-full"
                bind:value={todo.todoItem}
                on:change={updateTodo(todo)}
                on:input={updateTodo(todo)}
              />
            </div>
            <button on:click={deleteTodo(todo)} class="border-0"
              ><img src={deletIcon} alt="delete todo" class="w-6 " /></button
            >
          </li>
        {:else}
          <p>No goals for today!</p>
        {/each}
      </ul>
```

Notice how we are making use of the [{](https://svelte.dev/docs#if)[**#if**](https://svelte.dev/docs#if) [expression}](https://svelte.dev/docs#if) 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**](https://svelte.dev/docs#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` and `deleteTodo` function. We will do that later

## Create todo items

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:

```
      let todoItem = '';
    
      const addTodo = async () => {
        try {
          if (!todoItem) return alert('please add a goal for today!');
          const res = await axios.post('http://localhost:1337/todos', {
            todoItem,
            isCompleted: false,
          });
          // Using a more idiomatic solution
          todos = [...todos, res?.data];
          todoItem = '';
        } catch (e) {
          isError = e;
        }
```

    

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:

```
    <input
        type="text"
        bind:value={todoItem}
        class="w-full rounded-xl bg-white border-0 outline-none bg-opacity-10 p-4 shadow-lg mt-4"
        placeholder="Add new goals"
      />
    
    <button
        on:click={addTodo}
        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"
        ><span><img src={addIcon} alt="add todo" class="w-6 mr-4" /></span>Add new
        todo</button>
```


Notice the `bind:value={todoItem}` in our input field above. This binding is used to achieve two-way data binding in Svelte.

## Update todo items

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:

```
     const toggleComplete = async (todo) => {
        const todoIndex = todos.indexOf(todo);
        try {
          const { data } = await axios.put(
            `http://localhost:1337/todos/${todo.id}`,
            {
              isCompleted: !todo.isCompleted,
            }
          );
          todos[todoIndex].isCompleted = data.isCompleted;
        } catch (e) {
          isError = e;
        }
      };
```


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:

```
     const updateTodo = async (todo) => {
        const todoIndex = todos.indexOf(todo);
        try {
          const { data } = await axios.put(
            `http://localhost:1337/todos/${todo.id}`,
            {
              todoItem: todo.todoItem,
            }
          );
          todos[todoIndex].todoItem = data.todoItem;
        } catch (e) {
          isError = e;
        }
      };
```


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:

```
    <div class="flex items-center w-full">
              <input
                type="checkbox"
                class="mr-3"
                bind:checked={todo.isCompleted}
                on:click={toggleComplete(todo)}
              />
              <input
                class:completed={todo.isCompleted}
                class="border-0 bg-transparent w-full"
                bind:value={todo.todoItem}
                on:change={updateTodo(todo)}
                on:input={updateTodo(todo)}
              />
            </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](https://svelte.dev/docs#class_name) 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:

```
    <style>
      .completed {
        text-decoration: line-through;
      }
    </style>
```

## Delete todo items

Next, let’s create a function to delete items from our API and `todos` array:

```
     const deleteTodo = async (todo) => {
        try {
          await axios.delete(`http://localhost:1337/todos/${todo.id}`);
          todos = todos.filter((to) => to.id !== todo.id);
        } catch (e) {
          isError = e;
    }
```


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:

```
    <button on:click={deleteTodo(todo)} class="border-0"><img src={deletIcon} alt="delete todo" class="w-6 " /></button>
```

## Conclusion

In this article, we've seen how powerful and very easy to use Strapi is. Setting up a [backend project](https://masteringbackend.com/posts/backend-development-the-ultimate-guide/) 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](https://github.com/ejirocodes/Svelte-Tailwind-Todo), and [the backend code is available on GitHub](https://github.com/ejirocodes/Strapi-Todo-API). You can also find me on [Twitter](https://twitter.com/ejirocodes), [LinkedIn](https://www.linkedin.com/in/ejiro-asiuwhu/), and [GitHub](https://github.com/ejirocodes). Thank you for reading! Feel free to drop a comment to let me know what you thought of this article.