<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Abdulrahman Yusuf - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Abdulrahman Yusuf - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 18 May 2026 10:46:57 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/young_einstein10/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Web Apps with Nuxt and Laravel ]]>
                </title>
                <description>
                    <![CDATA[ The Laravel framework is one of the most widely used technologies in the web development ecosystem. It's relatively straightforward, and it's easy to use for building websites.  Laravel is built upon PHP, a popular web programming language that’s use... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-web-apps-with-nuxt-and-laravel/</link>
                <guid isPermaLink="false">66b9d9c738655574e9f95267</guid>
                
                    <category>
                        <![CDATA[ Laravel ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Nuxt.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Applications ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdulrahman Yusuf ]]>
                </dc:creator>
                <pubDate>Wed, 07 Feb 2024 10:59:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/pexels-pixabay-270557.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The Laravel framework is one of the most widely used technologies in the web development ecosystem. It's relatively straightforward, and it's easy to use for building websites. </p>
<p>Laravel is built upon PHP, a popular web programming language that’s used in over 75% of websites on the web. So knowing how to use a framework like Laravel can help make you a sought-after developer – and it also makes building websites and applications more seamless. </p>
<p>Nuxt is a Vue.js framework used for building rich and interactive web applications. It lets you choose between different rendering modes depending on the application requirements you want to build. You can choose between building a fully server-rendered app or client-rendered app. Nuxt also offers a mixture of both rendering modes, making applications much more powerful, efficient, and interactive.</p>
<p>In this article, you'll learn how to build full-stack applications using Nuxt and Laravel by building a Book Library App. The app will comprise a library API that we'll build using Laravel and a frontend using Nuxt. </p>
<p>We’ll talk about:</p>
<ul>
<li>Installing and setting up Laravel</li>
<li>Creating database models</li>
<li>Migrations</li>
<li>Controllers</li>
<li>API Testing</li>
<li>Form Validations</li>
<li>Data Fetching in Nuxt</li>
</ul>
<p>And more. Get ready, and let's dive in.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents:</strong></h2>
<ul>
<li><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-laravel-on-your-machine">How to set up Laravel on your machine</a></li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-book-library-api">How to build the Book Library API</a></p>
<ul>
<li><a class="post-section-overview" href="#setting-up-our-database">Setting up our database</a></li>
<li><a class="post-section-overview" href="#creating-our-book-model">Creating our Book Model</a></li>
<li><a class="post-section-overview" href="#creating-our-book-controller">Creating our Book Controller</a></li>
<li><a class="post-section-overview" href="#defining-our-api-routes">Defining our API Routes</a></li>
<li><a class="post-section-overview" href="#testing-our-api">Testing our API</a><ul>
<li><a class="post-section-overview" href="#heading-creating-a-new-book-1">Creating a new book</a></li>
<li><a class="post-section-overview" href="#getting-our-list-of-books">Getting our list of books</a></li>
<li><a class="post-section-overview" href="#editing-our-book">Editing our book</a></li>
</ul>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-the-nuxt-frontend">How to Build the Nuxt Frontend</a></p>
</li>
<li><a class="post-section-overview" href="#heading-how-to-integrate-the-laravel-api-in-the-frontend">How to Integrate the Laravel API in the Frontend</a><ul>
<li><a class="post-section-overview" href="#getting-all-books-in-our-library">Getting all books in our library</a></li>
<li><a class="post-section-overview" href="#heading-creating-a-new-book-1">Creating a new book</a></li>
<li><a class="post-section-overview" href="#heading-editing-a-book">Editing a book</a></li>
<li><a class="post-section-overview" href="#heading-deleting-a-book">Deleting a book</a></li>
</ul>
</li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
<li><a class="post-section-overview" href="#heading-resources">Resources</a></li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<ol>
<li><a target="_blank" href="https://www.php.net">PHP</a> and <a target="_blank" href="https://getcomposer.org">Composer</a> are installed on your local machine.</li>
<li><a target="_blank" href="https://nodejs.org/">Node.js</a> is installed on your local machine.</li>
<li><a target="_blank" href="https://yarnpkg.com/">yarn</a> or <a target="_blank" href="https://www.npmjs.com">npm</a> installed on your local machine (npm comes pre-installed with Node).</li>
<li>A text editor installed, like <a target="_blank" href="https://code.visualstudio.com">VSCode</a>.</li>
<li>Basic knowledge of HTML, CSS, JavaScript, and the terminal.</li>
<li>Basic knowledge of PHP, Vue.js, and TypeScript.</li>
</ol>
<h2 id="heading-how-to-set-up-laravel-on-your-machine"><strong>How to Set Up Laravel on Your Machine</strong></h2>
<p>To begin the Laravel installation, open your terminal and bootstrap a new Laravel project using the command below: </p>
<pre><code class="lang-bash">composer create-project laravel/laravel library-api &amp;&amp; <span class="hljs-built_in">cd</span> library-api &amp;&amp; code .
</code></pre>
<p>This command creates a new Laravel project in your directory. <code>cd</code>'s into it, and open up VSCode using the <code>code .</code> command. (If you happen to be using a different editor, you can remove that command and open the directory manually).</p>
<p>To test your Laravel server and ensure everything works, let’s test the server by using the <code>php artisan serve</code> command in the terminal. This should make your API available on port 8000 and accessible in your browser.</p>
<p><img src="https://lh7-us.googleusercontent.com/Wd1K8FLTnBwThyGwMl8lYeTmpO2886t4IN2lJ6Nv2tShwvw0HGxNWhl3cneeAEVVdVL_Gvf9sB0feecPHqpRpXYbPz-dBRPPAnBxJAabWyMySW-FqSwJkUi1_bTOX7fLqo1luWJIRi1iEPlbSkVxh3U" alt="Image" width="1600" height="865" loading="lazy">
<em>Laravel Starter Application</em></p>
<h2 id="heading-how-to-build-the-book-library-api"><strong>How to Build the Book Library API</strong></h2>
<p>We will be creating a simple CRUD endpoint for our book library, that is the API should be able to achieve the following:</p>
<ul>
<li>Create new book entries</li>
<li>Get a list of all the book entries in the database</li>
<li>Edit previously added book entries</li>
<li>Delete book entries from the database</li>
</ul>
<h3 id="heading-setting-up-the-database">Setting up the database</h3>
<p>Laravel comes with a variety of databases you can use during development. By default, it automatically creates a configuration setup for MySQL. But for simplicity, we’ll be making use of a <a target="_blank" href="https://www.sqlite.org/index.html">SQLite</a> database in this tutorial. </p>
<p>Open the <code>.env</code> file in your root directory, change the <code>DB_CONNECTION</code> value from <code>mysql</code> to <code>sqlite</code>, and comment the remaining database configurations as you can see below:</p>
<p><img src="https://lh7-us.googleusercontent.com/NoxsSV5jfFxxzAsKDX04xAfWzUAESGajpJnam3zTA3LpBIil9-esqfxk4I1CJXjlK_jyjEk3twDTGu_Bi_Efw1DUXaU0yUcdCFcdKECCZ-V-hT4Y_03YKfeQCz8JQ5MVSznXcC3dYei8vBb9wTCo1_c" alt="Image" width="1018" height="294" loading="lazy">
<em>Environmental Variables (available in .env file)</em></p>
<p>Laravel automatically helps create the SQLite database when the migrations command is run. We’ll do that in a bit after creating our database models.</p>
<h3 id="heading-creating-the-book-model"><strong>Creating the Book model</strong></h3>
<p>When you're building backend APIs, a model acts as a template that’s used to set up your database tables. It contains high-level instructions for how the tables and columns should be created. </p>
<p>In Laravel, you can use the command below to create a <code>Book</code> model:</p>
<pre><code class="lang-bash">php artisan make:model Book -m
</code></pre>
<p>This creates your Book model in the <code>app/models</code> directory and also creates a new migration file for you in the <code>database/migrations</code> directory.</p>
<p>Navigate to the migrations directory and open the newly created migrations file. It should be in the format <code>current_date_create_books_table.php</code> like you can see below:</p>
<p><img src="https://lh7-us.googleusercontent.com/zW84PQRgiGuLMX9yvmCJ_NvDS4xZfraKtxh7g0Du6rhOoqSpuXGlWnxkCSlTT9yhqT3c6N_jfPAsiuKakLG_MULYgYD6_Qe-Eycxo4hxaozonEVzbjVe64ddSu2FOpdOGMexms1jPuSZc43HB-N0-hY" alt="Image" width="1600" height="1016" loading="lazy">
<em>Default Migrations</em></p>
<p>Edit the <code>up</code> function and add the following content:</p>
<pre><code class="lang-php">$table-&gt;string(<span class="hljs-string">'title'</span>);
$table-&gt;string(<span class="hljs-string">'author'</span>);
$table-&gt;string(<span class="hljs-string">'isbn'</span>);
$table-&gt;date(<span class="hljs-string">'published_date'</span>);
$table-&gt;text(<span class="hljs-string">'cover_image'</span>)-&gt;nullable();
$table-&gt;foreignIdFor(User::class);
</code></pre>
<p>Your migrations file should look like this now:</p>
<p><img src="https://lh7-us.googleusercontent.com/sgSaSbourHadcXleC2Jxn8DHAkulytaRIQfcAZC7ZUPvNTJJITJDr9rDEeYSNf-mNjJxxW285Kz1C5MQlFWBNlSMG7Gk0VEIShz8-AtaA6hRHChRuYYkrYQkIl9IwZ_I0-tO7cXMFd8f3OLen-bAm9o" alt="Image" width="1600" height="1240" loading="lazy">
<em>Updated Migrations</em></p>
<p>In summary, you added new fields to be created for the Book tables. <code>Title</code>, <code>author</code>, and <code>isbn</code> all have a data type of string, while <code>published_date</code> and <code>cover_image</code> have data types of date and text, respectively. </p>
<p>You also made the <code>cover_image</code> nullable, that is the field can be left out whenever a new book entry is created. </p>
<p>Lastly, you imported the <code>User</code> class and made it a <a target="_blank" href="https://www.educative.io/blog/what-is-foreign-key-database">foreign key</a> to your Book table, creating a many-to-one relationship with the User table.</p>
<p>Now, let’s create your tables by running the migrations file with the command below:</p>
<pre><code class="lang-php">php artisan migrate
</code></pre>
<p>You should get a prompt in the terminal asking if you want to create your SQLite database before the migrations. Select yes and press enter to proceed with the migrations.</p>
<p>Once the migration has been run successfully, navigate to the Book model and paste in the following code:</p>
<pre><code class="lang-php"> <span class="hljs-comment">/**
* The attributes that are mass assignable.
*
* <span class="hljs-doctag">@var</span> array&lt;int, string&gt;
*/</span>

<span class="hljs-keyword">protected</span> $fillable = [
    <span class="hljs-string">'user_id'</span>,
    <span class="hljs-string">'title'</span>,
    <span class="hljs-string">'author'</span>,
    <span class="hljs-string">'isbn'</span>,
    <span class="hljs-string">'published_date'</span>,
    <span class="hljs-string">'cover_image'</span>
];
</code></pre>
<p>This will allow mass assignment when passing data into the model (passing the data as a whole instead of individually). Your Book model should look like this now:</p>
<p><img src="https://lh7-us.googleusercontent.com/zjYcbtyiQXKFRJubxnDuL5M6PRyEI5u5PaHGHUAfYgju5A8lQqOJAxRSLdsaIkg0bjFCXOOUTBGHlQXuJhpuEfOW5K6XbPDl2aEQoUyNvjs-p-m5TfRQhF2dlnwnZKHDb0EXsR6JneIGIXCw0ol1plI" alt="Image" width="1600" height="779" loading="lazy">
<em>Updated Book Model</em></p>
<h3 id="heading-creating-the-book-controller"><strong>Creating the Book controller</strong></h3>
<p>The controller is responsible for housing the business logic for any API being created. The implementation logic for how we want to perform any CRUD operations with the library API will live here. </p>
<p>Laravel also provides a command for you to make this easier for developers. Paste the command below in your terminal to create our book controller:</p>
<pre><code class="lang-php">php artisan make:controller BookController
</code></pre>
<p>This should be accessible at <code>app\Http\Controllers\BookController.php</code>. Replace the content of your BookController class with the code snippet below:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>

<span class="hljs-keyword">namespace</span> <span class="hljs-title">App</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Controllers</span>;

<span class="hljs-keyword">use</span> <span class="hljs-title">App</span>\<span class="hljs-title">Models</span>\<span class="hljs-title">Book</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Carbon</span>\<span class="hljs-title">Carbon</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Http</span>\<span class="hljs-title">Request</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Log</span>;
<span class="hljs-keyword">use</span> <span class="hljs-title">Illuminate</span>\<span class="hljs-title">Support</span>\<span class="hljs-title">Facades</span>\<span class="hljs-title">Storage</span>;


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BookController</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span>
</span>{
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">store</span>(<span class="hljs-params">Request $request</span>)
    </span>{
        $data = $request-&gt;validate([
            <span class="hljs-string">'title'</span> =&gt; <span class="hljs-string">'required|string'</span>,
            <span class="hljs-string">'author'</span> =&gt; <span class="hljs-string">'required|string'</span>,
            <span class="hljs-string">'isbn'</span> =&gt; <span class="hljs-string">'required|string|unique:books'</span>,
            <span class="hljs-string">'published_date'</span> =&gt; <span class="hljs-string">'required|string'</span>,
            <span class="hljs-string">'cover_image'</span> =&gt; <span class="hljs-string">'nullable|image|max:2048'</span>,
        ]);

        <span class="hljs-keyword">try</span> {
            $published_date = Carbon::parse($data[<span class="hljs-string">'published_date'</span>])-&gt;toDateString();
            $data[<span class="hljs-string">'published_date'</span>] = $published_date;
            $data[<span class="hljs-string">'user_id'</span>] = <span class="hljs-number">1</span>; <span class="hljs-comment">// Making the assumption the id of the user is 1 </span>
        } <span class="hljs-keyword">catch</span> (\<span class="hljs-built_in">Exception</span> $e) {
            <span class="hljs-keyword">return</span> response()-&gt;json([<span class="hljs-string">'Error'</span> =&gt; <span class="hljs-string">'Bad Request'</span>], <span class="hljs-number">400</span>);
        }

        $book = <span class="hljs-keyword">new</span> Book($data);

        <span class="hljs-keyword">if</span> ($request-&gt;hasFile(<span class="hljs-string">'cover_image'</span>)) {
            $coverImage = $request-&gt;file(<span class="hljs-string">'cover_image'</span>);
            $coverImageName = time() . <span class="hljs-string">'.'</span> . $coverImage-&gt;getClientOriginalExtension();
            $coverImage-&gt;move(public_path(<span class="hljs-string">'images'</span>), $coverImageName);
            $book-&gt;cover_image = $coverImageName;
        }

        $book-&gt;save();

        <span class="hljs-keyword">return</span> response()-&gt;json([<span class="hljs-string">'message'</span> =&gt; <span class="hljs-string">'Book added successfully'</span>], <span class="hljs-number">201</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">index</span>(<span class="hljs-params"></span>)
    </span>{
        $books = Book::all();

        <span class="hljs-keyword">return</span> response()-&gt;json([<span class="hljs-string">'books'</span> =&gt; $books], <span class="hljs-number">200</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">update</span>(<span class="hljs-params">Request $request, $id</span>)
    </span>{
        $book = Book::findOrFail($id);
        $data = $request-&gt;all();

        <span class="hljs-keyword">try</span> {
            $published_date = Carbon::parse($data[<span class="hljs-string">'published_date'</span>])-&gt;toDateString();
            $data[<span class="hljs-string">'published_date'</span>] = $published_date;
            $data[<span class="hljs-string">'user_id'</span>] = $book-&gt;user_id;
        } <span class="hljs-keyword">catch</span> (\<span class="hljs-built_in">Throwable</span> $th) {
            Log::error(<span class="hljs-string">'Error '</span>, $th);
            <span class="hljs-keyword">return</span> response()-&gt;json([<span class="hljs-string">'Error'</span> =&gt; <span class="hljs-string">'Bad Request'</span>], <span class="hljs-number">400</span>);
        }

        $book-&gt;update($data);

        <span class="hljs-keyword">if</span> ($request-&gt;hasFile(<span class="hljs-string">'cover_image'</span>)) {
            <span class="hljs-comment">// Delete the previous cover image if it exists</span>
            <span class="hljs-keyword">if</span> ($book-&gt;cover_image) {
                Storage::delete(<span class="hljs-string">'public/images/'</span> . $book-&gt;cover_image);
            }

            $coverImage = $request-&gt;file(<span class="hljs-string">'cover_image'</span>);
            $coverImageName = time() . <span class="hljs-string">'.'</span> . $coverImage-&gt;getClientOriginalExtension();
            $coverImage-&gt;storeAs(<span class="hljs-string">'public/images'</span>, $coverImageName);
            $book-&gt;cover_image = $coverImageName;
            $book-&gt;save();
        }

        <span class="hljs-keyword">return</span> response()-&gt;json([<span class="hljs-string">'message'</span> =&gt; <span class="hljs-string">'Book updated successfully'</span>], <span class="hljs-number">200</span>);
    }

    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">destroy</span>(<span class="hljs-params">$id</span>)
    </span>{
        $book = Book::findOrFail($id);

        <span class="hljs-comment">// Delete the cover image if it exists</span>
        <span class="hljs-keyword">if</span> ($book-&gt;cover_image) {
            Storage::delete(<span class="hljs-string">'public/images/'</span> . $book-&gt;cover_image);
        }

        $book-&gt;delete();

        <span class="hljs-keyword">return</span> response()-&gt;json([<span class="hljs-string">'message'</span> =&gt; <span class="hljs-string">'Book deleted successfully'</span>], <span class="hljs-number">200</span>);
    }
}
</code></pre>
<h3 id="heading-defining-the-api-routes"><strong>Defining the API Routes</strong></h3>
<p>Routing is one of the most important parts of building an API. Routes serve as access points through which developers, clients, or any other services can access an API. </p>
<p>The Laravel router allows you to register any routes that correspond to any HTTP verb being requested.</p>
<pre><code class="lang-php">Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);
</code></pre>
<p>Navigate to the <code>routes/api.php</code> file and create the following routes for your API:</p>
<pre><code class="lang-php">Route::post(<span class="hljs-string">'/books'</span>, [BookController::class, <span class="hljs-string">'store'</span>]);
Route::get(<span class="hljs-string">'/books'</span>, [BookController::class, <span class="hljs-string">'index'</span>]);
Route::put(<span class="hljs-string">'/books/{id}'</span>, [BookController::class, <span class="hljs-string">'update'</span>]);
Route::delete(<span class="hljs-string">'/books/{id}'</span>, [BookController::class, <span class="hljs-string">'destroy'</span>]);
</code></pre>
<h3 id="heading-testing-the-api"><strong>Testing the API</strong></h3>
<p>Now, it’s time to test your API and ensure all your endpoints are accessible and working as expected. </p>
<p>Run the Laravel server using the command below:</p>
<pre><code class="lang-php">php artisan serve
</code></pre>
<p>You can test the API using any API client of your choice. We have quite a number of popular options to choose from – <a target="_blank" href="https://www.postman.com">Postman</a>, <a target="_blank" href="https://insomnia.rest">Insomnia</a>, <a target="_blank" href="https://www.thunderclient.com">Thunder Client</a>, and so on. I’ll be using Thunder Client here.</p>
<p>We’ll test for all CRUD operations in your API client by confirming if all your objectives for your API are met.</p>
<h3 id="heading-creating-a-new-book">Creating a new book</h3>
<p><img src="https://lh7-us.googleusercontent.com/pUy0u6hCbOEihAsLhLTtSS6p0l3a42PIKHceWRnq5m4-0MJHwwG2r4mL5Y8MCr_EG3g7AuvvmW6nEx9SZ4-GtHTQNFO5Som8f4PYBPjntJSTNJXzrV0sbjL9RYRGhILUWHo6eWFb9tX82XWmrwH70yI" alt="Image" width="1600" height="1096" loading="lazy">
<em>Create book endpoint</em></p>
<h3 id="heading-getting-a-list-of-books">Getting a list of books</h3>
<p><img src="https://lh7-us.googleusercontent.com/9QNUQRrxFzime9Dsl9bKuh82o0XzD6AY8fUaAmQB_mHP0-pyqviLOVPfesLUsINl9n54tYqbjPNOWYEkJwfoXkaVuREbjGhqhghYI5ifi8e7TTQrNSIIm4jU3rM_8akLtc4so-4AzeegJicMpL8IGZw" alt="Image" width="1600" height="1095" loading="lazy">
<em>Get books endpoint</em></p>
<h3 id="heading-editing-a-book">Editing a book</h3>
<p><img src="https://lh7-us.googleusercontent.com/ZTC04dUEXPF2BdxNH-l9RbfzjhkMqO338pQSHjcoEl_hfa3EZbRUtWS2Stgk7slAlwjXpl1_ypvALbuQX3AvLaDPO0gj2rzh5BpqO-uwK6yjSehztm1wJnrjWLzKM0-asFqOU2wMOTp1xffqVroewec" alt="Image" width="1600" height="939" loading="lazy">
<em>Edit book endpoint</em></p>
<h2 id="heading-how-to-build-the-nuxt-frontend">How to Build the Nuxt Frontend</h2>
<p>Setting up a new Nuxt app is as simple as running the command below:</p>
<pre><code class="lang-bash">npx nuxi@latest init nuxt-library-frontend
</code></pre>
<p>Open the newly created project when you’re done installing all the dependencies and start the development server using this command:</p>
<pre><code class="lang-bash">npm run dev
</code></pre>
<p>Your Nuxt app should now be accessible on port 3000:</p>
<p><img src="https://lh7-us.googleusercontent.com/ZOV-VAcrLLTW7i6SAmFNh5q8hRcxhY05wC4vZUXbxTwgR5tTsKop-An1SYlFdXEiquu1zCz1tx5TTwrY9WlDEPuiuNpHh4vDsVWwlUVGvZFWljmsTGgRYST3478Pk79jNcNeXJoGa7lQcAdbTx3YitI" alt="Image" width="1600" height="873" loading="lazy">
<em>Nuxt Starter Page</em></p>
<p>This is what your frontend should look like at the end of the tutorial:</p>
<p><img src="https://lh7-us.googleusercontent.com/FbnijAJw1D-VJTnT8VNPQBfl694-fW2PhYTqoEokTMxceEnsN5Oa-mBTmSAcoeU8xcqRFyDceWTQjElj43oidAg2xtW2KWSVamDbL0kmVDuCZktQc1mER5pbCbGkshNsYm0WJKqbgQy8GVymZ8-Bo5I" alt="Image" width="1600" height="871" loading="lazy">
<em>Sample of our completed application</em></p>
<p>I’ve created some <a target="_blank" href="https://github.com/Young-Einstein10/laravel-nuxt-app">boilerplate code</a> to help with the Nuxt frontend setup. This will enable us to focus on the implementation logic for consuming the Laravel API rather than spending time setting things up and styling. </p>
<p>For knowledge's sake, I created the UI components for the app using <a target="_blank" href="https://www.shadcn-vue.com">shadcn-vue</a>. It consists of an amazing collection of accessible, reusable UI components that you can customize to your taste. Check out the <a target="_blank" href="https://www.shadcn-vue.com/docs/installation/nuxt.html">installation</a> steps for more details on how easy it was to set up for the Nuxt app. </p>
<p>It also comes with an installation of the <a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a> utility library that’ll we'll use for styling in this tutorial.</p>
<p>Clone the boilerplate setup from the GitHub repo <a target="_blank" href="https://github.com/Young-Einstein10/laravel-nuxt-app">here</a> so we can get started.</p>
<p>Navigate to <code>pages/index.vue</code> and replace the content of the index page with this:</p>
<pre><code class="lang-vue">&lt;template&gt;
 &lt;main class="bg-white p-10 min-h-screen"&gt;
   &lt;div class="max-w-4xl mx-auto"&gt;
     &lt;header class="flex justify-between items-center mb-20"&gt;
       &lt;h1 class="font-semibold text-4xl"&gt;My Library&lt;/h1&gt;
       &lt;Button&gt;Add New Book&lt;/Button&gt;
     &lt;/header&gt;


     &lt;div v-if="isLoading"&gt;
       &lt;p class="italic text-2xl font-medium text-center"&gt;Loading...&lt;/p&gt;
     &lt;/div&gt;
     &lt;div class="mt-8"&gt;
       &lt;div class="flex flex-col gap-8"&gt;
         &lt;div class="flex gap-4" v-for="book in books"&gt;
           &lt;div
             v-if="book.cover_image"
             class="rounded-lg w-32 h-44 flex items-center justify-center"
           &gt;
             &lt;NuxtImg
               :src="`${API_BASE_URL}/images/${book.cover_image}`"
               :alt="book.title"
               class="w-full h-auto"
             /&gt;
           &lt;/div&gt;
           &lt;div v-else class="bg-slate-300 rounded-lg w-32 h-44"&gt;&lt;/div&gt;
           &lt;div class="flex items-center justify-between flex-1"&gt;
             &lt;div&gt;
               &lt;h3 class="font-medium text-xl mb-4"&gt;{{ book.title }}&lt;/h3&gt;
               &lt;p class="mb-2"&gt;
                 &lt;span&gt;by:&lt;/span&gt;
                 &lt;span class="italic"&gt; {{ book.author }} &lt;/span&gt;
               &lt;/p&gt;
               &lt;p&gt;
                 &lt;span&gt;Published:&lt;/span&gt;
                 {{ book.published_date }}
               &lt;/p&gt;
             &lt;/div&gt;
             &lt;div class="actions flex gap-4"&gt;
               &lt;Button&gt;Edit&lt;/Button&gt;
               &lt;Button variant="destructive"&gt; Delete &lt;/Button&gt;
             &lt;/div&gt;
           &lt;/div&gt;
         &lt;/div&gt;
       &lt;/div&gt;
     &lt;/div&gt;
   &lt;/div&gt;
 &lt;/main&gt;
&lt;/template&gt;


&lt;script lang="ts" setup&gt;
import { API_BASE_URL } from "@/utils/constants";
import type { BookProps } from "@/utils/types";


const isLoading = ref(false);
const books = ref&lt;BookProps[]&gt;([]);
&lt;/script&gt;
</code></pre>
<p>We’ll need to create a new folder called <code>utils</code> in your root project which will contain two files: <code>constants.ts</code> and <code>types.ts</code>. Paste the following code snippet in your <code>constants.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> API_BASE_URL = <span class="hljs-string">"http://localhost:8000"</span>;
</code></pre>
<p>And the following in your <code>types.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> BookProps {
     id?: <span class="hljs-built_in">number</span>;
     title: <span class="hljs-built_in">string</span>;
     author: <span class="hljs-built_in">string</span>;
     isbn: <span class="hljs-built_in">string</span>;
     published_date: <span class="hljs-built_in">string</span>;
     cover_image?: <span class="hljs-built_in">string</span>;
     user_id?: <span class="hljs-built_in">number</span>;
     created_at?: <span class="hljs-built_in">string</span>;
     updated_at?: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>Save the <code>pages/index.vue</code> file. You should be able to see this result in your browser:</p>
<p><img src="https://lh7-us.googleusercontent.com/1M7J0_i-u7rUaHr6vX5na1sReyIh1e1s5VGFh1mZteE0j7sXM5lFUWBmPXZNDbLuobYM9nkJtodCvUTZig22KnJS471YHdOowjXmlmMBwkBuYpQ0J10rgi_xmJt8Vyfm_tHqsTEh6wdtqLx1q1X7KDc" alt="Image" width="1600" height="871" loading="lazy">
<em>Scaffolding the library frontend with Nuxt</em></p>
<p>Now, we need to create a side drawer and a dialog for adding new books and deleting added books from the database, respectively. </p>
<p>Create a new file called <code>BookDrawer.vue</code> in the component's root and add the following code in there:</p>
<pre><code class="lang-vue">&lt;script setup lang="ts"&gt;
import type { BookProps } from "@/utils/types";


type PickedProps = "title" | "author" | "isbn" | "published_date";
interface CustomBookProps extends Pick&lt;BookProps, PickedProps&gt; {
 cover_image?: string;
}


const props = defineProps&lt;{ open: boolean; book?: BookProps }&gt;();
const emit = defineEmits(["update:open", "refresh-data"]);


const isSubmitting = ref(false);
let form = reactive&lt;CustomBookProps&gt;({
 title: "",
 author: "",
 isbn: "",
 published_date: "",
 cover_image: undefined,
});


const onFileChange = async (e: any) =&gt; {
 form.cover_image = e.target.files[0];
};


const onSubmit = async () =&gt; {};


const closeDrawer = (openState: boolean) =&gt; emit("update:open", openState);
&lt;/script&gt;


&lt;template&gt;
 &lt;Sheet :open="open" @update:open="closeDrawer"&gt;
   &lt;SheetContent class="w-full bg-white"&gt;
     &lt;SheetHeader&gt;
       &lt;SheetTitle&gt;
         &lt;template v-if="book"&gt; Edit Book &lt;/template&gt;
         &lt;template v-else&gt;Add New Book&lt;/template&gt;
       &lt;/SheetTitle&gt;
       &lt;SheetDescription&gt;
         Make changes to your profile here. Click save when you're done.
       &lt;/SheetDescription&gt;
     &lt;/SheetHeader&gt;
     &lt;div class="grid gap-4 py-4"&gt;
       &lt;div class="grid grid-cols-4 items-center gap-4"&gt;
         &lt;Label for="bookTitle" class="text-right"&gt; Book Title &lt;/Label&gt;
         &lt;Input
           v-model:model-value="form.title"
           type="text"
           id="bookTitle"
           class="col-span-3"
           required
         /&gt;
       &lt;/div&gt;
       &lt;div class="grid grid-cols-4 items-center gap-4"&gt;
         &lt;Label for="author" class="text-right"&gt; Author &lt;/Label&gt;
         &lt;Input
           v-model:model-value="form.author"
           type="text"
           id="author"
           class="col-span-3"
           required
         /&gt;
       &lt;/div&gt;
       &lt;div class="grid grid-cols-4 items-center gap-4"&gt;
         &lt;Label for="isbn" class="text-right"&gt; ISBN &lt;/Label&gt;
         &lt;Input
           v-model:model-value="form.isbn"
           type="text"
           id="isbn"
           class="col-span-3"
           required
         /&gt;
       &lt;/div&gt;
       &lt;div class="grid grid-cols-4 items-center gap-4"&gt;
         &lt;Label for="published_date" class="text-right"&gt;
           Published Date
         &lt;/Label&gt;
         &lt;Input
           v-model:model-value="form.published_date"
           type="date"
           id="published_date"
           class="col-span-3"
           required
         /&gt;
       &lt;/div&gt;
       &lt;div class="grid grid-cols-4 items-center gap-4"&gt;
         &lt;Label for="cover_image" class="text-right"&gt; Book Cover &lt;/Label&gt;


         &lt;Input
           type="file"
           @change="onFileChange"
           id="cover_image"
           class="col-span-3"
           required
         /&gt;
       &lt;/div&gt;
     &lt;/div&gt;
     &lt;SheetFooter&gt;
       &lt;Button type="submit" @click="onSubmit"&gt;
         &lt;template v-if="isSubmitting"&gt;Saving...&lt;/template&gt;
         &lt;template v-else&gt; Save changes &lt;/template&gt;
       &lt;/Button&gt;
     &lt;/SheetFooter&gt;
   &lt;/SheetContent&gt;
 &lt;/Sheet&gt;
&lt;/template&gt;
</code></pre>
<p>Create another file called <code>DeleteBookDrawer.vue</code> and add the following code content to it: </p>
<pre><code class="lang-vue">&lt;script lang="ts" setup&gt;
const emit = defineEmits(["refresh-data", "update:open"]);
const props = defineProps&lt;{ open: boolean; book?: BookProps }&gt;();
&lt;/script&gt;


&lt;template&gt;
 &lt;AlertDialog
   :open="open"
   @update:open="(openState) =&gt; $emit('update:open', openState)"
 &gt;
   &lt;AlertDialogContent&gt;
     &lt;AlertDialogHeader&gt;
       &lt;AlertDialogTitle&gt;Are you absolutely sure?&lt;/AlertDialogTitle&gt;
       &lt;AlertDialogDescription&gt;
         This action cannot be undone. This will permanently delete your book
         and remove your data from the server.
       &lt;/AlertDialogDescription&gt;
     &lt;/AlertDialogHeader&gt;
     &lt;AlertDialogFooter&gt;
       &lt;AlertDialogCancel @click="$emit('update:open', false)"
         &gt;Cancel&lt;/AlertDialogCancel
       &gt;
       &lt;AlertDialogAction&gt;Continue&lt;/AlertDialogAction&gt;
     &lt;/AlertDialogFooter&gt;
   &lt;/AlertDialogContent&gt;
 &lt;/AlertDialog&gt;
&lt;/template&gt;
</code></pre>
<p>Import the newly created files in your index page as you can see below:</p>
<pre><code class="lang-vue">&lt;template&gt;
 &lt;main class="bg-white p-10 min-h-screen"&gt;
    ...

   &lt;BookDrawerDialog
     v-if="isBookDrawerOpen"
     :open="isBookDrawerOpen"
     @update:open="(open: boolean) =&gt; isBookDrawerOpen = open"
     :book="currentBook"
     @refresh-data="fetchBooks"
   /&gt;
   &lt;DeleteBookDialog
     v-if="isDeleteBookDialogOpen"
     :open="isDeleteBookDialogOpen"
     @update:open="(open: boolean) =&gt; isDeleteBookDialogOpen = open"
     :book="currentBook"
     @refresh-data="fetchBooks"
   /&gt;
 &lt;/main&gt;
&lt;/template&gt;
</code></pre>
<p>Now, we need to create a system for opening and closing the drawer and dialog, respectively. We’ll be creating a <a target="_blank" href="https://vuejs.org/guide/essentials/reactivity-fundamentals.html#ref">ref</a> to track the open/closed state for the drawer and dialog components in your index page. Paste the following code content in the script stage to enable that:</p>
<pre><code class="lang-vue">&lt;script lang="ts" setup&gt;

...

const currentBook = ref&lt;BookProps&gt;();


const isBookDrawerOpen = ref(false);
const isDeleteBookDialogOpen = ref(false);


const toggleAddBookDrawer = () =&gt; {
 isBookDrawerOpen.value = !isBookDrawerOpen.value;
};


const toggleEditBookDrawer = (book: BookProps) =&gt; {
 currentBook.value = book;
 isBookDrawerOpen.value = !isBookDrawerOpen.value;
};


const toggleDeleteDialog = (book: BookProps) =&gt; {
 currentBook.value = book;
 isDeleteBookDialogOpen.value = !isDeleteBookDialogOpen.value;
};
&lt;/script&gt;
</code></pre>
<p>Update the <code>Edit</code> and <code>Delete</code> buttons in your index page to include the toggle handler:</p>
<pre><code class="lang-vue">    ...

             &lt;div class="actions flex gap-4"&gt;
               &lt;Button @click="toggleEditBookDrawer(book)"&gt;Edit&lt;/Button&gt;
               &lt;Button @click="toggleDeleteDialog(book)" variant="destructive"&gt;
                 Delete
               &lt;/Button&gt;
             &lt;/div&gt;

           ...
</code></pre>
<p>Include the <code>toggleAddDrawer</code> in your Add Book button too:</p>
<pre><code class="lang-vue">&lt;Button @click="toggleAddBookDrawer"&gt;Add New Book&lt;/Button&gt;
</code></pre>
<p>Save the file changes and view in your browser. You should have the drawer active now:  </p>
<p><img src="https://lh7-us.googleusercontent.com/82AHDkwCX-oTW6pOpcwi735-8i5HluzloFnwDGnidH5gNKP8dkj3pXxr-Oa6gpMcQGZA-FnzsCP5nzpP0uGPDMhyTMkNqvxLXnVaK0TWFTBG-IrQPGU74-vhPpqCz1y7CcRULG7AUWcJsmejfdR5O2k" alt="Image" width="1600" height="816" loading="lazy">
<em>Add New Book Drawer</em></p>
<p>Moving on, we need to be sure the EditDrawer and Delete Dialog work. We’ll create a temporary array of mock book data to test that so you can see the result on your page. Update the value of the <code>books</code> ref to the data you have below (and don’t forget to update the books ref value to an empty array – that is <code>books = ref&lt;BookProps&gt;([])</code>):</p>
<pre><code class="lang-vue">const books = ref&lt;BookProps[]&gt;([
     {
       title: "Atomic Habits",
       author: "James Clear",
       isbn: "XYEOUIUEHEJ2902",
       published_date: "2020-09-01",
       cover_image: "",
     },
     {
       title: "The Power of Habits",
       author: "Charles Duhigg",
       isbn: "WIUIQUEWHSDBSD28",
       published_date: "2012-02-28",
       cover_image: "",
     },
]);
</code></pre>
<p>Save the changes and check out the EditDrawer and DeleteDialog in action:</p>
<p><img src="https://lh7-us.googleusercontent.com/_kiROvQBeLBq1UV9Zj4n0tVYOdW9nLUUjb5qlxGtgFGvAJtHsGJ0CvoEyYHxB4BIQ31SznXfwkiXhg_5VowECEMuSCYnIhfEHWeiHAt-hhA9dETiyTOgqNjsxxAenIhx0gl2koYaLVGYnohdpTV13KU" alt="Image" width="1519" height="824" loading="lazy">
<em>Delete dialogue</em></p>
<p><img src="https://lh7-us.googleusercontent.com/YSTKDAEOpLp45EXn61OlRYTTW3hYmOF-ur3SES7emDWMyigINQNvpAozgscEF84-RgiZrgCkTIxtZehFYDMIntzWmy589khRcP7FV4OpdacfXvsi8xRlJG9qWBuXc8Csbal0C_Tr-_lrDt6AatSnVIg" alt="Image" width="1513" height="821" loading="lazy">
<em>EditDrawer</em></p>
<h2 id="heading-how-to-integrate-the-laravel-api-in-the-frontend">How to Integrate the Laravel API in the Frontend</h2>
<h3 id="heading-getting-all-books-in-the-library">Getting all books in the library</h3>
<p>So far, we only have the UI of our frontend working, and the data being displayed is not from the API. To make the app fully dynamic, you'll need to make an HTTP request to the server to get all the data you need for your page. </p>
<p>Update the body of the <code>fetchBooks</code> function in the index page to the following code:</p>
<pre><code class="lang-vue">const fetchBooks = async () =&gt; {
 try {
   isLoading.value = true;
   const response = await $fetch&lt;{ books: BookProps[] }&gt;(
     `${API_BASE_URL}/api/books`
   );


   books.value = response.books.sort(
     (a, b) =&gt;
       Number(new Date(b.created_at as string)) -
       Number(new Date(a.created_at as string))
   );


 } catch (error) {
   console.log(error);
 } finally {
   isLoading.value = false;
 }
};


onMounted(() =&gt; {
 fetchBooks();
});
</code></pre>
<p>Basically, you're calling your <code>fetchBooks</code> function when the page mounts using one of the <a target="_blank" href="https://vuejs.org/api/composition-api-lifecycle.html#onmounted">lifecycle hooks</a> available in Vue.js. When the function is called, it executes the instruction you created in the body:</p>
<ul>
<li>Set the loading state of the app to true.</li>
<li>Make a GET HTTP request to the books endpoint of your API and set the response to the books ref you created earlier.</li>
<li>When the request-response has been completed, set the loading state of the app back to false to indicate to the user the process has been completed.</li>
</ul>
<p>Save the changes, and you should see the results in the browser:</p>
<p><img src="https://lh7-us.googleusercontent.com/SGyND7gBGa7NmUXM4k-7Jn8pyuRizWhMJOOGnwZMMe7NIz6Jzt_qC0D3JUf-7X9IlGJMdm8XLbzGHG8cuKcJ6z83XpxOrrSGdAgOFaaUd6tDmo6-bDY-3PZG1lLRW4mywF77A86ABoIggR6NGwDDK5A" alt="Image" width="1509" height="818" loading="lazy">
<em>Fetching the book list</em></p>
<h3 id="heading-creating-a-new-book-1">Creating a new book</h3>
<p>For your book creation to be successful, you need to include the logic for creating a book in your <code>BookDrawer</code>. Add the following code in the <code>BookDrawer.vue</code> file:</p>
<pre><code class="lang-vue">...

const isSubmitting = ref(false);


const addNewBook = async (data: any) =&gt; {
 const response = await $fetch(`${API_BASE_URL}/api/books`, {
   method: "POST",
   body: data,
   headers: {
     Accept: "application/json",
   },
 });
 return response;
};


const onSubmit = async () =&gt; {
 try {
   const formData = new FormData();
   Object.keys(form).forEach((key) =&gt; {
     // @ts-ignore
     if (form[key]) {
       formData.append(key, form[key as never]);
     }
   });
   isSubmitting.value = true;
   const data = await addNewBook(formData);
   closeDrawer(false);
   emit("refresh-data");
 } catch (error) {
   console.log(error);
 } finally {
   isSubmitting.value = false;
 }
};

...
</code></pre>
<p>Similar to the implementation for fetching your books, the only difference here is that you're making a POST HTTP request instead. You use this method whenever you need to mutate data on the server.</p>
<p><img src="https://lh7-us.googleusercontent.com/7zQhG_V35mPWJW9sxMyTP8QjnezzoVg1eX08pdJ3Mw7EiXPwcHiTLdYHUD8HWqXg54SjvXymxrBCVWUjXbiuVNCAdj5vccdPyFl6KFJuuKAFTC9sYUQkkGYnESKr3aVHgUONnyOqAf1-fkafeqgyifE" alt="Image" width="1600" height="778" loading="lazy">
<em>Creating a new book</em></p>
<h3 id="heading-editing-a-book-1">Editing a book</h3>
<p>Update your <code>BookDrawer</code> with the following code to enable editing of the books that have been added:</p>
<pre><code class="lang-vue">&lt;script setup lang="ts"&gt;

...

onMounted(() =&gt; {
 if (props.book) {
   form = props.book;
 }
});


const editBook = async () =&gt; {
 try {
   isSubmitting.value = true;
   const resp = await $fetch(`${API_BASE_URL}/api/books/${props?.book?.id}`, {
     method: "PUT",
     body: formData,
   });
   closeDrawer(false);
   emit("refresh-data");
 } catch (error) {
   console.log(error);
 } finally {
   isSubmitting.value = false;
 }
};

...
&lt;/script&gt;
</code></pre>
<p>Update the save button with the toggle handler also:</p>
<pre><code class="lang-vue">&lt;Button
  type="submit"
  @click="() =&gt; (book?.id ? editBook() : onSubmit())"
&gt;
  &lt;template v-if="isSubmitting"&gt;Saving...&lt;/template&gt;
  &lt;template v-else&gt; Save changes &lt;/template&gt;
&lt;/Button&gt;
</code></pre>
<p>And here's the result:</p>
<p><img src="https://lh7-us.googleusercontent.com/aUGh8K6y9u8qXRwckj_PRphjvbnTR2DXf8jnHVehD2aaKtaCdCK1SVPjW-AbWwp_CXZROabKsTk0JBCaUjLAAzEbDHlFCbG6sjkpcVdW9VaKPqoYkVp2aoEtec5vPpjxFtNVAdFFWp3E3T_ZWNdxtYc" alt="Image" width="1600" height="867" loading="lazy">
<em>Updating a book</em></p>
<h3 id="heading-deleting-a-book">Deleting a book</h3>
<p>Paste the following in the <code>DeleteBookDialog.vue</code> file:</p>
<pre><code class="lang-vue">const refreshData = () =&gt; emit("refresh-data");
const closeDialog = () =&gt; emit("update:open", false);


const deleteBook = async () =&gt; {
 await $fetch(`${API_BASE_URL}/api/books/${props?.book?.id}`, {
   method: "DELETE",
 });
 closeDialog();
 refreshData();
};
</code></pre>
<p>This works fine also:</p>
<p><img src="https://lh7-us.googleusercontent.com/KyP9Y8L7qzkvvQCfyuFSBCQvrV30hE-rpQ72Q2Z1-DL-6rzkXnFAuwWEAL0Y1t110slYV813RPXyTKlxxKG5gHFQitUdVuT_7a-gkPxpJXtYOtRsxHWI0JSt9nFDeM6ywNf3ReoCxeIsKzguzZosw_E" alt="Image" width="1600" height="816" loading="lazy">
<em>Deleting a book</em></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building full-stack web apps with Laravel and Nuxt is a very rewarding experience as you can enjoy the benefits and rewards from the best of both worlds. </p>
<p>Since Laravel is a feature-rich backend framework, it comes with many modules and packages that you can install whenever a feature or implementation needs to be worked on. </p>
<p>And Nuxt, as a Vue framework, gives you the superpower of building single-page applications that are fast, accessible, and responsive, giving end-users a delightful experience.</p>
<p>In this tutorial, we went over how we can build full-stack applications using Nuxt and the Laravel framework. I hope you were able to learn something new and are excited to explore this further. Feel free to reach out if you have any questions or ideas. </p>
<p>The full code for the tutorial can be found in the GitHub repo <a target="_blank" href="https://github.com/Young-Einstein10/laravel-nuxt-app/tree/final">here</a>.</p>
<h3 id="heading-resources">Resources</h3>
<ul>
<li><a target="_blank" href="https://laravel.com/docs/10.x">Laravel Docs</a></li>
<li><a target="_blank" href="https://nuxt.com/docs/getting-started/installation">Nuxt Documentation</a></li>
<li><a target="_blank" href="https://www.shadcn-vue.com">Shadcn-vue</a>, ready-made UI components that can be copy-pasted anytime</li>
<li><a target="_blank" href="https://vuejs.org">Vue Docs</a>  </li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
