Build a Svelte App that Uses the National Parks Service API to Plan Your Perfect Trip

Developers analizing an Svelte App
July 28, 2022
Written by
Reviewed by
Dainyl Cua
Twilion

The United States National Park Service, created in 1916, operates 423 unique sites that cover 85 million acres of land. Our national parks are some of the United States’ greatest treasures, and most people agree: in 2021, the National Park Service welcomed 297,115,406 visitors across its multitude of locations.

This summer, if you’re feeling bored, listless, or over the heat, plan a trip to a national park. There are parks to suit absolutely everyone’s needs, and in this article you’ll learn how to build a Svelte app that lets you leverage the National Parks API to search for your perfect national park by filtering through all possible activities and park amenities.

This article is going to be showing you how to build an app that you can run locally on your machine. The API requests shown in this article are inherently insecure, because if you were to deploy this app as written, your National Parks API key would be exposed on the client.

The goal of this app is to demonstrate concepts related to the National Parks API and Svelte, not API security, and it’s been simplified for that reason.

To keep your API credentials secure when using client-side frameworks, the recommendation is to use your own secure backend service to make requests and send the results back to your client. To learn more about how to secure your backend service, check out this documentation on basic auth and JSON web tokens.

Prerequisites

To get started with this tutorial, you’ll need the following:

Overview of the app

When the user visits the app in their browser they will be presented with a welcome screen that will look like this:

App welcome screen that says "bored? visit a national park."

When the user clicks Begin they will be presented with a screen where they can select activities that they would like to be able to do when they visit a national park:

Activity selection screen of the app

After the user makes their selections, they can hit the Previous button to start over, or the Next button to move to the second question:

Screenshot showing previous and next buttons at bottom of screen

After clicking Next, the user will be shown the second question, which asks them to select their needed amenities:

Screenshot showing "select amenities that are important to you" screen

After the user makes their selections, they can click Previous to be taken to the previous question, or Finish to be shown their results:

screenshot showing list of results

On the results screen, the user will be able to click on any of their results to be taken to that park’s website. They can also click a button that says Start Over to begin their search again.

Scaffold the app

To get started, open up a new terminal window, navigate to a suitable parent directory, and run the following command:

npm create vite@latest national-parks

This will prompt you to select a framework. Select svelte by hitting the down arrow key until svelte is selected and then hit the enter key.

You will then be prompted to select a variant. Select svelte, which is the default, and then hit enter. svelte-ts is for typescript projects, which won’t be covered in this article.

This command cloned a template app into your parent directory. All of the code is inside a folder called national-parks. To finish setting up your app, run the following commands:

cd national-parks
npm install

These commands navigate to your new app folder and install any required dependencies.

Create your Svelte stores file

Inside your new app folder is a folder called src. This is where most of your code files will go. Using your terminal, navigate into this src folder:

cd src

Stores are Svelte’s mechanism to share state and data across different, unrelated components. You’ll be using stores to keep track of the user’s progress through the app.

Create a file called stores.js by running the following command in your terminal, still within the src folder:

touch stores.js

In this app, the three pieces of state that will be managed inside of stores are: step, questions, and responseOptions.

step is an integer value that tracks how far the user is through the quiz. This piece of state has a default value of -1, which is the step value associated with the welcome screen. When the user moves to the next screen, step will increase by 1. Likewise, if they move to the previous screen, step will decrease by 1.

The questions state is an array of objects. Each object contains three values: the question to be displayed to the user, the request URL that should be made to get the corresponding response options from the National Parks API, and an empty array to store the user’s selections for that question.

Finally, responseOptions is a derived store that makes the appropriate request to the National Parks API based on the current step. All of the values returned from the request are accessible via this state object. Every time the value of step changes, so will the value of responseOptions.

To create these three stores, copy the following code and paste it into stores.js:


import { get, writable, derived } from 'svelte/store';

export const step = writable(-1);

export const questions = writable([
  {
    question: 'Select the activities that are important to you:',
    request: `https://developer.nps.gov/api/v1/activities?api_key=XXX`,
    selections: []
  },
  {
    question: 'Select the amenities that are important to you:',
    request: `https://developer.nps.gov/api/v1/amenities?limit=127&api_key=XXX`,
    selections: []
  }
]);

export const responseOptions = derived(step, ($step, set) => {
  set([]);
  const qs = get(questions);

  if ($step < qs.length ) {
    const getOptions = async() => {
      const response = await fetch(qs[$step].request);
      return await response.json();
    }
  
    getOptions().then(data => set(data.data));
  }
});

Be sure to replace the XXX placeholders on lines 8 and 13 with your actual API key from National Parks API.

In a production app, you would replace the request URLs in this code sample with request URLs to a secure backend where your API key could be stored in an environment variable. From these backend endpoints, you would make the actual requests to the National Parks API using the request URLs you see here.

You would amend the fetch request on line 24 to make the request to your backend more secure via basic auth or a JSON web token.

Create your component file structure

A file called App.svelte was already created for you inside the src folder. You’ll edit this file shortly. Before that though, create all your other components by running the following command in your terminal, still within the src folder:

touch Question.svelte ResponseOption.svelte Navigation.svelte NavigationButton.svelte Results.svelte

This command creates five files, one for each of your new components. These components are as follows:

Question.svelte will contain code for the Question component. This component will be used to show the user the two quiz questions: “Select the activities that are important to you” and “Select the amenities that are important to you”.

Nested as a child component of the Question component is ResponseOption, which renders an individual option that the user could select for each question, such as Wildlife Viewing for activities or ATM for amenities. These options are generated via the API.

Also nested inside the Question component are the Navigation component and its child component NavigationButton. These two components control user movement through the app via the Previous, Next, Finish, and Start Over buttons.

Finally, the Results component displays the user’s filtered results and the option to start the quiz over.

Edit the App component

Open App.svelte in your text editor and delete all of the code you find inside. Replace it with this:

<script>
  import {step, questions} from './stores.js';
  import Question from './Question.svelte';
  import Results from './Results.svelte';
  import Navigation from './Navigation.svelte';
  import NavigationButton from './NavigationButton.svelte';
</script>

<main>
  {#if $step == $questions.length}
    <Results />
  {:else if $step >= 0}
    <Question/>
  {:else}
    <h1>Bored? Visit a National Park.</h1>
    <Navigation>
      <NavigationButton text='Begin' targetStep={$step+1} />
    </Navigation>
  {/if}
</main>

The App component is the top-most component in the app and controls which screen the user sees.

Save and close this file.

Add code to the Question component

Open Question.svelte and paste in the following code:


<script>
  import {step, questions, responseOptions} from './stores.js';
  import ResponseOption from './ResponseOption.svelte';
  import Navigation from './Navigation.svelte';
  import NavigationButton from './NavigationButton.svelte';

  $: loading = $responseOptions.length > 0 ? false : true;
</script>

{#if loading}
  Loading...
{:else}
  <h2>{$questions[$step].question}</h2>

  {#each $responseOptions as option}
    <ResponseOption {option} />
  {/each}

  <Navigation >
    {#if $step >= 0}
      <NavigationButton text='Previous' targetStep={$step-1} />
    {/if}

    <NavigationButton text={$step < $questions.length - 1 ? 'Next' : 'Finish'} targetStep={$step+1} />
  </Navigation>
{/if}

This component will show a loading message while the responseOptions derived store is making its request to the National Parks API. Once the store is populated with response options, this component will render each of these options in its own ResponseOption component. Beneath the response options, it will render the navigation buttons.

There’s two Svelte specific features to be noted in this code:

On line 7, this variable assignment begins with $:. This is Svelte’s way of creating reactive code. Anytime any of the dependent values of this assignment change, so will the variable’s value itself. This is a very handy feature, because typically, the script will only run when the component loads the first time.

That means, without this reactivity, the value of loading would be static, regardless of whether or not the value of the responseOptions store changes. By adding $: to the beginning of the statement, loading will now update every time the current value of responseOptions changes.

Secondly, on line 19, the Navigation component is rendered, with a child component, NavigationButton inside it here instead of in its component file. This is demonstrating a feature of Svelte called slots. Slots allow you to customize the content of a component depending on its context, while still providing a consistent look and functionality.

In this case, the navigation area of each screen should be styled the same and function the same, but the buttons inside will be different depending on the user’s current step in the flow. Using slots makes this dynamic rendering easier to code. You’ll see what this looks like in the Navigation component file later in this article.

Save and close this file.

Add code to the ResponseOption component

Open the ResponseOption.svelte file found in the src folder. Paste the following code into the file:

<script>
  import {step, questions} from './stores.js';
  export let option;

  let selected = false;

  const handleClick = id => {
    if (!selected) {
      $questions[$step].selections = [...$questions[$step].selections, id];
      selected = true;
    } else {
      $questions[$step].selections = $questions[$step].selections.filter(selectionId => selectionId!=id);
      selected = false;
    }
  }
</script>

<button 
  on:click={()=>handleClick(option.id)}
  class:selected="{selected==true}">
    {option.name}
</button>

<style>
  button {
    margin: 10px;
  }

  .selected {
    background-color: blue;
    color: #f9f9f9;
  }
</style>

This code renders an individual ResponseOption component. As a prop it receives an option object, which contains details about the response option, including its id and its name. These details come from the API. Each option is rendered as a button that can be clicked to select or deselect it.

If an option is selected, then its id will be added to the selections array for the appropriate quiz question inside the questions store value. This is how the app keeps track of what the user has selected. Once the option has been selected, it can be deselected with a second click. If this happens, the id will be removed from the selections array.

Save and close this file.

Add code to the Navigation component

Open the Navigation.svelte file and paste in the following code:


<div class="navigation">
  <slot></slot>
</div>

<style>
  .navigation {
    margin-top: 50px;
  }
</style>

In the highlighted line above, the <slot></slot> is where Svelte will render the child content of Navigation that was included in the Question component. Slots can also have default content, in case nothing was provided.

Save and close this file.

Add code to the NavigationButton component

Open the NavigationButton.svelte file. Paste the following code inside the file:

<script>
  import {step, questions} from './stores.js';

  export let text;
  export let targetStep;

  const handleClick = () => {
    $step=targetStep;
    
    if (text == 'Start Over') {
      $questions.forEach(question => question.selections = []);
    }
  }
</script>

<button on:click={handleClick}>{text}</button>

<style>
  button {
    background-color: green;
    color: white;
  }
</style>

This code renders an individual navigation button whose text is provided as a prop. Also provided as a prop is the targetStep. When clicked, the navigation buttons will update the user’s step in the app flow based on whatever target step it receives as a prop.

Save and close the file.

Add code to the Results component

The final component, Results, has the most complicated code. In this component, the code will use the user’s selections, saved in the questions store, to query the National Parks API and filter the results. It will then render the results and a Start Over button that users can click to search for a park again. Paste the following code into the file, taking care to replace the placeholder API key value with your actual API key on lines 12 and 17.

Again, this app is meant to run locally on your machine. An API query like this, with the API key inline on a client application, is insecure.


<script>
  import {questions, step} from './stores.js';
  import Navigation from './Navigation.svelte';
  import NavigationButton from './NavigationButton.svelte';

  const selectedActivities = $questions[0].selections;
  const selectedAmenities = $questions[1].selections;
  let results = [];
  let loading = true;

  const getAllParks = async() => {
    const response = await fetch(`https://developer.nps.gov/api/v1/parks?api_key=XXX&limit=467`)
    return await response.json();
  }

  const getParksWithAmenities = async() => {
    const response = await fetch(`https://developer.nps.gov/api/v1/amenities/parksplaces?api_key=XXX&id=${selectedAmenities.toString()}`);
    return await response.json();
  }

  getAllParks().then(data => {
    const allParks = data.data;

    const parksWithActivities = allParks.filter(park => {
      const parkActivities = park.activities.map(activity => activity.id);
      if (selectedActivities.every(activity => parkActivities.includes(activity))) return park;
    });

    getParksWithAmenities().then(data => {
      //Flatten data to create parksWithAmenities, an array that only lists relevant park codes
      const response = data.data.flat().map(result => result.parks).flat();
      const parksWithAmenities = response.map(park => park.parkCode);

      results = parksWithActivities.filter(park => parksWithAmenities.includes(park.parkCode));
      loading = false; 
    });
  });
</script>
 

{#if loading}
  Results loading...
{:else}
  <h3>Your results are:</h3>  

  {#if results.length > 0}
    {#each results as result}
      <a href="{result.directionsUrl}">{result.name}</a><br>
    {/each}
  {:else}
    No results
  {/if}

  <Navigation>
    <NavigationButton text='Start Over' targetStep={0}/>
  </Navigation>
{/if}

The app needs to perform three processes: the first is to query for every park and filter that list for only parks that offer the activities the user selected; the second is to take the user’s required amenities to generate a list of parks that offer these amenities; the third is to compare these lists and only return parks that meet all the requirements.

The reason this code looks more complicated than the other component files is because of the way the data is returned from the National Parks API.

The API returns amenities data in a way that doesn’t lend itself easily to processing. The query returns an array of amenities objects. Each amenity object contains a nested array of park place objects, representing individual locations within a national park that have the requested amenity. From this park place object, you can obtain the park code required to cross reference against the filtered list of parks with the right activities.

Save and close this file, it’s time to test the app.

Test the app

In your terminal, navigate back to the root of your project. If you’re in the src directory, this command will be:

cd ..

Start your local server by running the following command:

npm run dev

Copy the URL next to Local: and paste it into your browser.

Console screenshot after running npm run dev showing localhost url

Enjoy, and have a fabulous time at the National Park that most perfectly suits you. For me, I’m off to Mammoth Cave in Kentucky, where I can paddle, look at the stars, view wildlife, have a drink at the end of a beautiful day and charge my electric car up before heading back home.

Ashley is a JavaScript Editor for the Twilio blog. To work with her and bring your technical stories to Twilio, find her at @ahl389 on Twitter. If you can’t find her there, she’s probably on a patio somewhere having a cup of coffee (or glass of wine, depending on the time).