D. Lytras
Dimitrios Lytras

Bootstrapping a Vue.js Project - Part 1 Structure

January 10, 2018
6 minutes read

Welcome to this series of articles on working your way through a new Vue.js project. I’ll break down the series into three (or possible more) parts as follows :

Part 1: Structure
Part 2: Initial configuration and caveats - TBD
Part 3: Best practices - TBD

One of the easiest ways to make sure your project won’t be overwhelming in the long run, is to set up some conventions from the very early on. Having a clear and true structure, will make all the pieces to come, fall into place and sweep maintaining nightmares away.

Thus, for the first part of this series of articles, I will talk about my preferred folder structure and set up some boilerplate code for the main pieces. Without any further ado, this is the main skeleton we’ll work on. Note that some files and folders that I’ve included (e.g datepicker.js, charts) are just there to fill the gaps and give you an idea.

.
├─ src
│  ├─ assets/
│  │  ├─ fonts/
│  │  ├─ styles/
│  │  │  ├─ partials/
│  │  │  │  ├─ _common.scss         # Common styling, e.g anchors.
│  │  │  │  ├─ (_tables.scss)       # Element stylings that get too lengthy
│  │  │  │  ├─ _layout.scss         # CSS not suitable in layout/components scoped css
│  │  │  │  ├─ _helpers.scss        # If not using bootstrap 4 helpers
│  │  │  │  ├─ _reset.scss          # Reset, Normalize, whatever
│  │  │  │  ├─ _typography.scss     # Typography specific
│  │  │  │  └─ _variables.scss      # Strictly variables
│  │  │  └─ main.scss
│  │  └─ svg/
|  ├─ components/
│  │  ├─ layout/                    # (The)Header, (The)Sidebar etc.
│  │  ├─ major/                     # Components that consist of 'minor' components
│  │  │  ├─ charts/
│  │  │  │  ├─ ChartBar.vue
│  │  │  │  ├─ ChartPie.vue
│  │  │  └─ ColorPicker.vue
│  │  │  minor/                     # Lowest level components
│  │  │  ├─ Avatar.vue
│  │  │  ├─ MessageBox.vue
│  │  │  └─ Rating.vue
│  ├─ utils/
│  │  ├─ api/
│  │  │  ├─ calls/
│  │  │  │  ├─ user.js
│  │  │  │  ├─ something.js
│  │  │  │  └─ else.js
│  │  │  └─ index.js                # Axios instances and interceptors
│  │  ├─ directives/
│  │  ├─ helpers/                   # Common functions and plugin configurations
│  │  │  ├─ filters.js              # Text transformations, moment functions, etc
│  │  │  ├─ colors.js               # Color combinations for charts.
│  │  │  └─ datepickers.js          # Datepicker options
│  │  ├─ i18n/
│  │  ├─ store/
│  │  │  ├─ modules/                # Always use modules, keep them seperate from views
│  │  │  │  ├─ user/                # Either keep a single index.js or if getting lengthy
│  │  │  │  └─ dropdowns/           # break into _actions.js, _mutations.js, etc
│  │  │  └─ index.js
│  │  ├─ router/
│  │  |  └─ index.js                # Totally dependable on the length of the project.
│  ├─ views/
│  │  ├─ landing/
│  │  │  └─ Landing.vue
│  |  ├─ interface/
│  │  │  ├─ view1/
│  │  │  │  ├─ subview/
│  │  │  │  ├─ _routes.js            # path & child views paths, imported in app/routes.js
│  │  │  │  ├─ View1.vue
│  │  │  ├─ view2/
│  │  │  ├─ view3/
│  │  │  └─ (interface)_routes.js   # imported in utils/router
│  ├─ AppWrapper.vue                # Main app component
│  └─ main.js                       # App entry file
├─ public/                          # static assets (no webpack)
│  └─ ...
├─ ...
├─ ...
└─ ...

Notice: No project skeleton is perfect.

Let there be style

Contrary to my love for PostCSS, I mostly use SASS and compliment it with any PostCSS plugin that seem appropriate. The partial SASS files categorization is not set in stone, but that’s the general idea. Anything that it’s really specific, put it the component’s scope.

Friendly reminder, use classes to target the appropriate elements.

The toolbox folder

I keep a folder called utils, where all the cool JS files hang out. There you can find the HTTP client instances & calls, the router configuration, as well as the store and it’s modules.

I also include a folder called helpers. From this part on, it’s up to you. Every bit of code that shouldn’t really be in a .vue file method, can be abstracted and called from there. At the very least, write your filters here.

Tip: Modifying Webpack configuration, it’s easy to set up an alias and expose this folder as ‘utils/something/something’ for easier access. Relative paths are like semicolons, it’s best to avoid them.

Do i need so many routes.js files? Actually I did not get it.

Consider the following:

Tennis/
  Friendly/
    _routes.js
    ..
  Lessons/
    _routes.js
    ..
  Tournaments/
    Tournament/
      _routes.js
      TournamentOverview.vue
      TournamentParticipants.vue
      TournamentSettings.vue
    Tournament.vue        # Just a 'router-view' wrapper
    TournamentsList.vue
    _routes.js
TennisOverview.vue
tennis_roots.js

Naming conventions aside, here’s what the single Tournament routes would be

import Overview from './TournamentOverview';
import Participants from './TournamentParticipants';
import Settings from './TourmamentSettings';

export default [
  {
    path: '',
    name: 'tournament-overview',
    component: Overview,
    meta: {
      requiresAuth: true,
    },
  },
  {
    path: 'participants',
    name: 'tournament-participants',
    component: Participants,
    meta: {
      requiresAuth: true,
    },
  },
  {
    path: 'settings',
    name: 'tournament-settings',
    component: Settings,
    meta: {
      requiresAuth: true,
    },
  },
];

With the children views, imported in the upper level.

import List from './TournamentsList';
import Single from './Tournament';

import tournamentRoutes from './Tournament/routes';

export default [
  {
    path: '',
    name: 'tournaments-list',
    component: List,
    meta: {
      requiresAuth: true,
    },
  },
  {
    path: ':tournamentid',
    component: Single,
    meta: {
      requiresAuth: true,
    },
    children: tournamentRoutes,
  },
];

If your app have very few screens, you can most likely get away having everything in utils/router. Otherwise it’s best to split everything into smaller pieces of code.

That way, if you need to rename or re-organise the children views, you don’t have to scroll past a huge route tree with irrelevant information. These routes are tightly coupled with the components, so there’s no point to have them elsewhere.

Ideally the utils/router should only export the Router and have some middle-ware logic for transitions between pages. Consider the following snippet for a simple Auth check before every transition.

router.beforeEach((to, from, next) => {
  if (!to.meta.requiresAuth || store.state.user.authenticated) {
    next();
  } else {
    next({ name: 'landing' });
  }
});

// and we use the meta tag as follows
// {
//   path: '/users',
//   name: 'users',
//   component: Users,
//   meta: { requiresAuth: true },
// },

What about the API helper?

Axios will be the HTTP client of our choice. We can initialize an instance of with some default properties and reuse it in every part of our application.

Taking it one step further, we can apply some middle-ware for better error handling.

import axios from 'axios';
import qs from 'qs';

const API_URL = process.env.API_URL;
// const OTHER_API_URL = pricess.env.OTHER_API_URL

axios.defaults.headers.common.Accept = 'application/json';

// Instances -----------------------------------------
export const apiService = axios.create({
  baseURL: API_URL,
});
// If needed, hasle free calls to another API
// export const otherService = axios.create({
//     baseURL: OTHER_API_URL
// })

// Interceptors -----------------------------------------
// ----- Before
apiService.interceptors.request.use(
  config => {
    config.paramsSerializer = params => {
      return qs.stringify(params, {
        arrayFormat: 'brackets',
      });
    };
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);
// ----- After
apiService.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response.status === 401 || error.response.status === 400) {
      // your prefered global error handling
    }
    return Promise.reject(error);
  }
);
// ------------------------------------------------------

export function setToken(token) {
  axios.defaults.headers.common.Authorization = `Bearer ${token}`;
}

What about the call themselves?

I like to keep the requests separate from the Vuex actions, in order to keep things modular and a bit more tidy.

import { apiService } from 'utils/api';

export const requestMemberInterests = params => {
  return apiService.get(`/path/to/happiness`, { params });
};

export const requestMemberSubscriptions = params => {
  return apiService.get(`/path/to/happiness`, { params });
};

// etc
import * as dropdowns from 'utils/api/dropdowns'

// ...
 async getMemberCategories ({ commit }, params) {
   const response = await dropdowns.requestMemberSubscriptions(params)
   commit('setMemberCategories', response.data)
}
// ..

I’m ready for kickoff

Vue.js CLI is pretty awesome for painless project initialization. I kick-start my projects with the Webpack template although Brunch seems promising) and some specific configurations I’ll go by in the next article.

Fin

Watch out for the next parts!

Thanks for reading!  ❤️
Due to privacy concerns, I've decided to remove Disqus.
Until I settle for another commenting solution, you can share your thoughts with me via e-mail
Back to Posts