A National Parks Service API Demo with Vue.js

A National Parks Service API Demo with Vue.js

This weekend I was on the road and had some time to build (yet another) application with Vue.js. I don't think this one necessarily does anything terribly cool. At minimum it was more "exercise" for my Vue muscles and provides another demo I can share with folks. As always though, if you have any suggestions or feedback in general, just let me know. If posts like these aren't helpful, also free free to share!

Let me start by giving a high level overview of what I built. I'll start with a few screen shots. The initial page shows a list of all fifty states.

List of 50 States

Selecting a state will then make a call out to the National Park Systems API to ask for all the parks within that state. I then render them out:

List of parks

Behind the scenes I'm using the following technologies:

  • Vue.js of course. :)
  • Vue Router
  • Vuex to handle calling my API and caching (this is somewhat interesting I think).
  • Vuetify for the UI.
  • Zeit for my serverless function.

Before I dig into the code more, you can find the complete repository here: https://github.com/cfjedimaster/vue-demos/tree/master/nps_gallery. You can run the demo here: https://npsgallery.raymondcamden.now.sh/

Alright, so I'm not going to share anything about the first view of this page. I've got a hard coded list of the 50 states (and abbreviations) I store in my Vuex store and I simply fetch them to render. The only part that was interesting here is that I discovered the <router-link> will correctly handle URL encoding values:

<v-btn color="teal" width="100%" :to="`/state/${state}/${abbr}`">

In the link above, note that I can safely use the state value without worry. I should have expected this, but I was happy to see it worked well.

It's the state view where things get interesting. First, the main view component, which is pretty simple since my complexity lies elsewhere.


      <h3>National Parks for {{state}}</h3>

      <i v-if="loading">Please stand by - loading data.</i>

        <v-col cols="4" v-for="(park,idx) in parks" :key="idx">
          <Park :park="park" />


import Park from '../components/Park';

export default {
  components: { Park },
  data() {
    return {
  computed: {
    loading() {
      return !this.parks.length;
    parks() {
      return this.$store.state.selectedParks;
  async created() {
    // clear selecion

    this.state = this.$route.params.state;
    this.abbr = this.$route.params.abbr;
    this.$store.dispatch('loadParks', this.abbr);

You can see I'm rendering values by binding to a parks variable that comes from my store. You'll notice I'm calling two things in my created related to the store. I first call clearSelection and then loadParks. clearSelection removes any previously loaded parks from the view and loadParks obviously fires off the request to load parks. Let's look at the store now because here is where things get a bit deep.

import Vue from 'vue'
import Vuex from 'vuex'


import api from './api/nps';

export default new Vuex.Store({
  state: {
      "AL": "Alabama",
	  // stuff removed here
      "WY": "Wyoming"

  mutations: {
    cache(state, args) {
      console.log('storing cache for '+args.abbr+ ' and '+args.parks.length + ' parks');
      state.parks[args.abbr] = args.parks;
    clearSelection(state) {
      state.selectedParks = [];
    select(state, parks) {
      state.selectedParks = parks
  actions: {
    async loadParks(context, abbr) {
      // check the cache
      if(context.state.parks[abbr]) {
        console.log('woot a cache exists');
        context.commit('select', context.state.parks[abbr]);
      } else {
        console.log('no cache, sad face');
        let results = await api.getParks(abbr);
        context.commit('cache', {abbr:abbr, parks:results});
        context.commit('select', context.state.parks[abbr]);

So the biggest thing I want to point here is that I'm using the store to wrap calls to my API and as a simple cache. Anytime you ask for parks for state X, I first see if it's cached and if so - return it immediately. Otherwise I make a call out to the API. It's a pretty simple system but I love how it came out, and performance wise it works really.

The API part is actually two fold. You can see I load in './api/nps', which is yet another wrapper:

const NPS_API = '/api/npswrapper';

export default {

    async getParks(state) {
        return new Promise(async (resolve, reject) =>{
          let results = await fetch(NPS_API+`?state=${state}`);
          let parks = await results.json();
            API returns park.images[], we want to change this to park.image to simplify it
          let parkData = parks.data.map(p => {
            if(p.images && p.images.length > 0) {
                p.image = p.images[0].url;
            return p;


All this does is call my serverless function. The NPS API doesn't support CORS so I need that to handle that aspect. I also do a bit of filtering to ensure we get images back. (Although this doesn't seem to work perfectly - I think some parks have images that 404.) The final bit is the serverless function:

const fetch = require('node-fetch');

const NPS_KEY = process.env.NPS_KEY;

module.exports = async (req, res) => {

    let state = req.query.state;
    let httpResult = await fetch(`https://developer.nps.gov/api/v1/parks?stateCode=${state}&limit=100&fields=images&api_key=${NPS_KEY}`);
    let results = await httpResult.json();


If you want to know more about serverless and Zeit, check out the article I wrote a few days on it.

Anyway, that's it! As I always say, I'd love some feedback, so leave me a comment below.

Header photo by Sebastian Unrau on Unsplash

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Peter Kassenaar posted on 9/18/2019 at 8:04 AM

Thanks for the article, very informative.

I had to add an (empty) .eslintrc however to the root of the cloned repo, to get it compiled.

Otherwise it would throw the Error: No ESLint configuration found message.


Comment 2 (In reply to #1) by Raymond Camden posted on 9/18/2019 at 6:23 PM

Sorry - I had the same issue when cloning to a new machine. When I can I'll commit that.

Comment 3 by Che Vilnonis posted on 10/8/2019 at 3:20 PM

Thanks for this post - my deployed version just works and looks great! However, my local version [using 'now dev'] shows errors in the console and nothing is displayed. They are 'is not a valid JavaScript MIME type' for chunk-vendors[xxxxx].js and app[xxxxx].js. The console also shows SyntaxError: expected expression, got '<'. Any ideas what I might be doing wrong to view the app locally?

Comment 4 (In reply to #3) by Raymond Camden posted on 10/8/2019 at 3:24 PM

If I remember right, I did not use "now dev" to run the *entire* local stack. I used it to run the local API, and used npm run dev to run the Vue app in a different port. Ie, I didn't use "now dev" for everything. I'll have to get back to you and see if I remember right.

Comment 5 (In reply to #4) by Che Vilnonis posted on 10/8/2019 at 4:57 PM

Makes sense. I tested and the Vue app/API do run separately. I tried editing 'nps.js' to use 'http://localhost:3000/api/npswrapper' but a CORS error message shows in the console. Hmmm. I'll keep trying...

Comment 6 (In reply to #5) by Raymond Camden posted on 10/8/2019 at 6:34 PM

So, I just tested it DOES work, although it's a bit slow to setup. Just now dev, and then hit the URL. Make sure you add a .env file NPS_KEY=X, where X is your key.