• Labs icon Lab
  • Core Tech
Labs

Guided: Nuxt Foundations

In this hands-on lab, you’ll work with a pre-existing application consisting of a catalog page, cart page and product page for an online robot shop. Along the way, you'll learn how to manage static assets effectively using the /public and /assets directories, implement state management for a seamless shopping cart experience, interact with APIs to fetch product details dynamically, and build dynamic routes to display those product details. Each step will guide you through modifying the application, reinforcing Nuxt.js fundamentals in a practical, real-world scenario.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 40m
Published
Clock icon Apr 08, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction & Getting Started

    Welcome to Joe's Robot Shop!

    In this lab, you'll be building Joe's Robot Shop, a website for purchasing robot parts to build robots!

    Your project currently has a catalog page and a cart page, but there's lots of missing pieces. In this lab you will:

    • Manage static assets, like images, with the public and assets folders to get images displaying in your app.
    • Build API handlers and fetch data with useFetch() to replace hard-coded data on the catalog page with data from your new API.
    • Manage application state with useState() to store cart state so you can add items to the cart from the catalog page and acess that shared cart data on the cart page.
    • Create dynamic routes to generate dynamic product info pages based on the product ID passed in the URL.

    To get started, start up your Nuxt dev server so you can browse around the existing app.

    Start the Nuxt Dev Server

    To get started, find the Terminal tab in the bottom right. In the Terminal, start the Nuxt dev server by either typing npm run dev or by clicking the Run button in the bottom right which will execute that command for you.

    Browse the Joe's Robot Shop App

    Once you have started the server, click this link to open the web browser in a new browser tab:

    Joe's Robot Shop (new tab)

    (Note: You can also use the Web Browser tab to see the running app and then click the icon in the top right of the browser tab to pop the browser out into a separate window.)

    info> If you get stuck on a task, there is a solution folder in the filetree which contains the completed solutions for each task in this lab.

  2. Challenge

    Static Assets: Using the Public Folder

    The public folder in the root of a Nuxt project is used for serving static assets like images or the robots.txt file that is consumed by web crawlers.

    If you look at the Joe's Robot Shop app in your browser, you'll notice that the logo image is missing/broken in the site header at the top. Also, on the catalog/home page you'll notice that all of the product images are also missing. You'll need to put images in the right place and then access them from your components.

    You can do this in two ways. The first, and simplest is to use the public folder. Files that are placed in the public folder are made public and are accessible from a web browser at the root url. For example, if you placed a robot.png file in the public folder of a website hosted at joesrobotshop.com, it could be accessed in the url at joesrobotshop.com/robot.png. Note that you do not include /public in the url.

    Take a look inside the /public folder in your project -- notice there is a logo.png image already in that folder. Since this is in the public folder, it is accessible at /logo.png. Use that image to display the logo image in the SiteHeader.

    Accessing Static Assets Dynamically

    Take a look at the catalog page in the browser. Notice that the images for each of the products are all missing/broken. If you look in the public folder you'll see there are already images for these in the /public/images/products directory but the app is not yet using them.

    The catalog page is the main home page, which means it's rendered from /pages/index.vue. If you look at that component you can see it is using v-for to loop over the products from products.json. It then uses the ProductDetails component to render each Product.

    Take a look at the products.json file and notice that each product object in the products array includes an image property with the .png filename for that product. Now, open up the ProductDetails component in /components/ProductDetails.vue. Notice that it is using an img element, but the src property is just bound to product.image. This means the source will be set to something like arm-grabber.png. But this needs to point to something like /images/products/arm-grabber.png.

  3. Challenge

    Static Assets: Using the Assets Folder

    The nice thing about the public directory is that it is super simple to use, but there is one downside. Assets in the public dir are not processed by web bundlers like Vite or Webpack. This means you can't handle things like minification (for JavaScript or CSS) or caching (for all types of assets).

    Using the assets folder (or really any folder other than public) to load assets allows them to be processed by the bundler, but it is a bit more complicated to use.

    info>Note: The assets folder is used by convention, but really there's nothing special about the assets folder. You could name this folder anything, and really all your assets don't even have to be in the same folder, you can load them from anywhere in your app, other than the public folder and have them be processed by your bundler.

    To reference a file as a build-time resource, you add a ~ to the front of the url. For example:

    src="~/assets/logo.png"

    The ~ refers to the root directory of your project and, in the example above, it makes it clear that you're targeting the assets folder in the root of the project. Without the ~, it would be unclear whether you're talking about the root assets folder or an assets folder in /public/assets.

    Notice that there is also a copy of the logo.png and all product images in the /assets folder. Update your /components/SiteHeader component to load the logo from the assets folder instead of from the public folder. ###### Accessing Static Assets Dynamically That was easy enough, but where things get a little tricky is when you want to dynamically reference images in the assets folder.

    Lets revisit the ProductDetails component. If you recall, you are now building the src of the image in this component dynamically with something like this:

    :src="`/images/products/${product.image}`"

    You are dynamically building the image URL with an expression. It would be nice if you could just preface that expression with ~/assets/..., but the challenge here is that the build/bundling tool (Vite) needs to know the full URL of all possible images, at build time, in order in order to bundle them. So, you actually need to tell the build tool to bundle all possible images that you may access in a given directory so that it can bundle them all at build time.

    You can do this with a special Vite function, import.meta.glob(), which will build, at build time, an object with properties for each asset in a given directory. For example, if you had an assets folder with two images, image1.png and image2.png, you could access them dynamically by first building an images object like this:

    const images = import.meta.glob('~/assets/*.png', { eager: true })
    

    This would create an object that looks like this:

    {
      '/assets/image1.png`: 
        { default: `/_nuxt/assets/image1.png` },
      '/assets/image2.png`: 
        { default: `/_nuxt/assets/image2.png` },
    }
    

    Notice that that object has two properties, whose property names are their simple paths in the assets directory. And their property value is basically an ES module (hence the default property). And the default property points to the URL where each image will be available at runtime.

    That's a little more difficult to explain than to demonstrate. What it actually means is that you can create the images object, as mentioned, like this:

    const images = import.meta.glob('~/assets/*.png', { eager: true })
    

    And then in your components, you can access those images dynamically like this:

    <img
      :src="images[`/assets/${imageName}`].default"
    />
    

    Use this approach to update the image in the /components/ProductDetails.vue component so that it dynamically imports the product images from the assets folder (the images are already in ~/assets/images/products/*).

  4. Challenge

    Using API Handlers

    Nuxt makes it super easy to create API endpoints and call them from your components. Currently, the Catalog component (/pages/index) is loading all of the product data from a hard-coded products.json file. It's more typical to get data like this from an API. Creating an API endpoint with Nuxt is simple.

    To create an api endpoint, you just add an api folder inside the existing server folder and export an eventHandler function which defines an api handler function. That function simply needs to return your data (or alternatively, a promise which returns the data). The name of the file you create becomes the url for your API endpoint.

    For example, the following api handler function would create an API endpoint which would return a http response with an JSON array of strings:

    export default eventHandler(() => {
      return ['apples',  'oranges', 'bananas']
    })
    

    If this file was created at: /server/api/products.js, you could then access it at the URL http://[domain]/api/products.

    Update your robots shop app so that it fetches it's product data from an API. First, create the API endpoint. Now update the Catalog component to fetch it's product data from your new API endpoint.

    Remember, there are two ways to fetch data with Nuxt: The $fetch() function and the useFetch() composable. The useFetch() composable should be used whenever you want to fetch data during the component's setup/ initialization.

    Take a look at the catalog component in /pages/index.vue. Since this component is loading the data in the setup script, useFetch() is the correct choice.

    As a reminder, you can fetch data with useFetch like this:

    const { data } = await useFetch('/api/endpoint`)
    

    And you can even alias the data variable like this so you can use a more meaningful variable name like 'products' instead of 'data':

    const { data: products } = await useFetch('/api/endpoint`)
    
  5. Challenge

    State Management

    Your application has a catalog page and a cart page. However, the cart page currently is just showing a single hard-coded product, and clicking the Buy button on the catalog page doesn't really do anything. In other words, if you click buy on the Catalog page and then navigate to the Cart page, nothing shows up in the cart other than the single hard-coded product.

    If you look at the Catalog component (/pages/index.vue), you'll see that there is a Buy button with a @click handler that is empty. What should happen is the buy button should add the corresponding product to a shareable cart state. That way, when a user navigates to the cart page you access that shared state from the Cart page to see the items added from the Catalog page.

    When you want to create state that is shareable across multiple pages, you use useState().

    The useState() function takes in 2 parameters, a unique string identifier for that state, and, optionally, a function to set the initial value of the state like this:

    const state = useState('myState', () => [])
    

    That creates a global piece of state with the key myState and initializes it to an empty array. useState then returns a ref which points to the stored state, so you could then mutate that state like this:

    state.value.push('apple')
    

    The global myState state would now be set to an array with a single entry of apple. Great, you're now storing data in your global cart state, now update the cart page to access the cart state.

    In any componnent, you can access shared state that was created and initialized in another component. The most simple (although generally not recommended) way to do this is without an initialization function like this:

    //Not generally recommended
    const myState = useState('myState')
    

    warning>In the example above, notice there is no initialization function passed in as the second property. This can work fine, but you have to be aware that if the user lands on this page first, then myState will remain uninitialized until the user navigates to a page where the state is initialized. Often it's best to initialize your state everywhere to avoid accidentally using uninitialized state. Adding an initialization function won't overwrite existing state if state was initialized or populated previously.

    So typically when accessing state on a component, you'll provide an initialization function for safety reasons as follows:

    const myState = useState('myState', () => [])
    
  6. Challenge

    Creating Dynamic Routes

    Many applications have dynamic routes where data is passed on the URL. For example, the url https://mydomain.com/users/abc123 passes the user ID of abc123 on the URL. The application would then use that user ID from the URL to display something like a profile or account page for that user.

    Of course, in Nuxt, routing is defined by the folder and file structure inside the pages folder. So a page for the URL /users/abc123 would typically live at /pages/users/abc123.vue, but the challenge here is that user ID portion of that URL is dynamic and it's unreasonable to create a separate page for every user of your app. So, this is where dynamic pages come into play.

    Rather of using an actual user ID for the name of the component (i.e. abc123.vue), you can instead use square brackets and the name of the parameter as the file name for example, [userId].vue.

    Then, inside your component script, you can access that userId using the useRoute() composable like this:

    const route = useRoute()
    const userId = route.params.userId
    

    If you take a look in the pages folder of this app, you can see there is a product-info folder and currently it has an index.vue component. So, if you click this link to browse to /product-info in your browser. You can see that currently renders a static page for a spring base.

    If you look at the code for this component (/pages/product-info/index.vue), you'll notice that productId is hard-coded to 17. Using the info above, update this component so that it is a dynamic component and gets this productId from the URL using the useRoute() composable.

    info>You may have noticed that, in the product-info component, the productId is hardcoded to a string '17' even though productIds in the products.json file are numbers. And you may have noticed that in the products.find expression it is using a double equals == instead of a triple equals ===. This is because values retrieved from the URL area always strings so the code was setup in advance to handle this. Great, with the task above completed, you now have a dynamic Product Info page. Now update your app so that anytime a user clicks on the product image it takes them to the dynamic product info page for that product.

    The ProductDetails child component in /components/ProductDetails.vue is responsible for rendering the product details including product images. Update this component with a NuxtLink around the image element that links to your new dynamic product info page. Congratulations on completing the Nuxt Foundations lab! In this lab you demonstrated:

    • Managing static assets with the /public and /assets folders;
    • creating API handlers and fetching data from an API with useFetch();
    • managing application state across multiple components;
    • and, creating and navigating to dynamic routes!

Jim Cooper is a software developer at Pluralsight. With more than 20 years of software development experience, he has gained a passion for Agile software development -- especially Lean.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.