Build fullstack Javascript apps with Adonis and Vue
Tags: vue, adonis, node, laravel
Published: 2018-10-08 07:21:06

---
title: Build fullstack Javascript apps with Adonis and Vue
published: true
description: We build an app (SPA) with Adonis and Vue on a single repository
tags: vue, adonis, node, laravel
cover_image: https://thepracticaldev.s3.amazonaws.com/i/jo7menm45by2enaw0t8x.png
canonical_url: https://michaelzanggl.com/articles/build-fullstack-js-apps-with-adonis-and-vue
---

> Originally posted at [michaelzanggl.com](https://michaelzanggl.com/articles/build-fullstack-js-apps-with-adonis-and-vue). Subscribe to [my newsletter](https://michaelzanggl.com/) to never miss out on new content.

Today we want to bring together two amazing frameworks that allow us to build clean applications using only Javascript. 
[Adonis](https://adonisjs.com/) is a Laravel inspired web framework for Node, which carries over many of Laravel's features like an SQL ORM, authentication, migrations, mvc structure, etc. 
[Vue](https://vuejs.org/) is a frontend web framework to build single page applications (SPA) or just in general, apps that require interactivity. Just like React, it changes the way you think about and design the frontend.

You can find the code to this tutorial here.

{% github MZanggl/adonis-vue-demo %}

## Project Setup

#### Install Adonis CLI

```bash
npm install -g @adonisjs/cli
```


#### Create Adonis Project

```bash
adonis new fullstack-app
cd fullstack-app
```

## Webpack

#### File structure

We want to create all our frontend JavaScript and Vue files inside `resources/assets/js`. Webpack will transpile these and place them inside `public/js`.
Let's create the necessary directory and file

```bash
mkdir resources/assets/js -p
touch resources/assets/js/main.js
```

```javascript
// resources/assets/js/main.js

const test = 1
console.log(test)
```

## Get Webpack Rolling

People who come from a Laravel background might be familiar with `Laravel-Mix`. The good thing is that we can use Laravel Mix for our Adonis project as well. It takes away much of the configuration hell of webpack and is great for the 80/20 use case.
Start by installing the dependency and copy `webpack.mix.js` to the root directory of the project.

```bash
npm install laravel-mix --save
cp node_modules/laravel-mix/setup/webpack.mix.js .
```


`webpack.mix.js` is where all our configuration takes place. Let's configure it

```javascript
// webpack.mix.js

let mix = require('laravel-mix');

// setting the public directory to public (this is where the mix-manifest.json gets created)
mix.setPublicPath('public')
// transpiling, babelling, minifying and creating the public/js/main.js out of our assets
    .js('resources/assets/js/main.js', 'public/js')



// aliases so instead of e.g. '../../components/test' we can import files like '@/components/test'
mix.webpackConfig({
    resolve: {
        alias: {
            "@": path.resolve(
                __dirname,
                "resources/assets/js"
            ),
            "@sass": path.resolve(
                __dirname,
                "resources/assets/sass"
            ),
        }
    }
 });
```

Also, be sure to remove the existing example to avoid crashes

```javascript
mix.js('src/app.js', 'dist/').sass('src/app.scss', 'dist/');
```

#### Adding the necessary scripts

Let's add some scripts to our `package.json` that let us transpile our assets. Add the following lines inside `scripts`.

```javascript
// package.json

"assets-dev": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"assets-watch": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development webpack --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
"assets-hot": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development webpack-dev-server --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
"assets-production": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=production webpack --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
```

We can execute `npm run assets-watch` to keep a watch over our files during development. Running the command should create two files: `public/mix-manifest.json` and `public/js/main.js`. It is best to gitignore these generated files as they can cause a lot of merge conflicts when working in teams...

## Routing

Since we are building a SPA, Adonis should only handle routes that are prefixed with `/api`. All other routes will get forwarded to vue, which will then take care of the routing on the client side.
Go inside `start/routes.js` and add the snippet below to it

```javascript
// start/routes.js

// all api routes (for real endpoints make sure to use controllers)
Route.get("hello", () => {
    return { greeting: "Hello from the backend" };
}).prefix("api")
Route.post("post-example", () => {
    return { greeting: "Nice post!" };
}).prefix("api")

// This has to be the last route
Route.any('*', ({view}) =>  view.render('app'))
```
Let's take a look at this line: `Route.any('*', ({view}) =>  view.render('app'))`

The asterisk means `everything that has not been declared before`. Therefore it is crucial that this is the last route to be declared.

The argument inside `view.render` `app` is the starting point for our SPA, where we will load the `main.js` file we created earlier. Adonis uses the Edge template engine which is quite similar to blade. Let's create our view

```bash
touch resources/views/app.edge
```

```html
// resources/views/app.edge

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Adonis & Vue App</title>
</head>
<body>
    <div id="app"></div>
    {{ script('/js/main.js') }}
</body>
</html>
```

The global `script` function looks for files inside `resources/assets` and automatically creates the script tag for us.

## Vue Setup

Let's install vue and vue router

```bash
npm install vue vue-router --save-dev
```

And initialize vue in `resources/assets/js/main.js`

```javascript
// resources/assets/js/main.js

import Vue from 'vue'
import router from './router'
import App from '@/components/layout/App'

Vue.config.productionTip = false


new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})
```

In order to make this work we have to create `App.vue`. All layout related things go here, we just keep it super simple for now and just include the router.

```bash
mkdir resources/assets/js/components/layout -p
touch resources/assets/js/components/layout/App.vue
```

```vue
// /resources/assets/js/components/layout/App.vue

<template>
    <router-view></router-view>
</template>

<script>
export default {
  name: 'App'
}
</script>
```

We also have to create the client side router configuration

```bash
mkdir resources/assets/js/router
touch resources/assets/js/router/index.js
```

```javascript
// resources/assets/js/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
    mode: 'history', // use HTML5 history instead of hashes
    routes: [
        // all routes
    ]
})
```

Next, let's create two test components inside `resources/assets/js/components` to test the router.

```bash
touch resources/assets/js/components/Index.vue
touch resources/assets/js/components/About.vue
```

```vue
// resources/assets/js/components/Index.vue

<template>
    <div>
        <h2>Index</h2>
        <router-link to="/about">To About page</router-link>
    </div>
</template>

<script>
export default {
    name: 'Index',
}
</script>
```

And the second one

```vue
// /resources/assets/js/components/About.vue

<template>
    <div>
        <h2>About</h2>
        <router-link to="/">back To index page</router-link>
    </div>
</template>

<script>
export default {
    name: 'About',
}
</script>
```
The index component has a link redirecting to the about page and vice versa.
Let's go back to our router configuration and add the two components to the routes.

```javascript
// resources/assets/js/router/index.js

// ... other imports
import Index from '@/components/Index'
import About from '@/components/About'

export default new Router({
    // ... other settings
    routes: [
        {
            path: '/',
            name: 'Index',
            component: Index
        },
        {
            path: '/about',
            name: 'About',
            component: About
        },
    ]
})
```

## Launch

Let's launch our application and see what we've got. Be sure to have `npm run assets-watch` running, then launch the Adonis server using

```bash
adonis serve --dev
```

By default Adonis uses port 3333, so head over to `localhost:3333` and you should be able to navigate between the index and about page.
Try going to `localhost:3333/api/hello` and you should get the following response in JSON: `{ greeting: "Nice post!" }`.

## Bonus

We are just about done, there are just a few minor things we need to do to get everything working smoothly:
- CSRF protection
- cache busting
- deployment (Heroku)

#### CSRF protection

Since we are not using stateless (JWT) authentication, we have to secure our *POST*, *PUT* and *DELETE* requests using [CSRF protection](https://adonisjs.com/docs/4.1/csrf). Let's try to fetch the POST route we created earlier. You can do this from the devtools.

```javascript
fetch('/api/post-example', { method: 'post' })
```

The response will be somthing like `POST http://127.0.0.1:3333/api/post-example 403 (Forbidden)` since we have not added the CSRF token yet. Adonis saves this token in the cookies, so let's install a npm module to help us retrieving it.

```bash
npm install browser-cookies --save
```

To install npm modules I recommend shutting down the Adonis server first.

Next, add the following code to `main.js`

```javascript
// resources/assets/js/main.js

// ... other code

import cookies from 'browser-cookies';

(async () => {
    const csrf = cookies.get('XSRF-TOKEN')
    const response = await fetch('/api/post-example', {
        method: 'post',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json',
            'x-xsrf-token': csrf,
        },
    });

    const body = await response.json()

    console.log(body)
})()
```

This should give us the desired result in the console! I recommend extracting this into a module. Of course you can also use a library like axios instead.


#### Cache Busting

[Cache Busting](https://www.keycdn.com/support/what-is-cache-busting) is a way to make sure that our visitors always get the latest assets we serve.
To enable it, start by adding the following code to `webpack.mix.js`

```javascript
// webpack.mix.js

mix.version()
```

If you *restart* `npm run assets-watch`, you should see a change inside `mix-manifest.json`
```json
// public/mix-manifest.json

{
    "/js/main.js": "/js/main.js?id=e8f10cde10741ed1abfc"
}
```


Whenever we make changes to `main.js` the hash will change. Now we have to create a hook so we can read this JSON file in our view.

```bash
touch start/hooks.js
```

```javascript
const { hooks } = require('@adonisjs/ignitor')
const Helpers = use('Helpers')

const mixManifest = require(Helpers.publicPath('mix-manifest.json'))

hooks.after.providersBooted(async () => {
    const View = use('View')
    View.global('versionjs', (filename) => {
        filename = `/js/${filename}.js`
        if (!mixManifest.hasOwnProperty(filename)) {
            throw new Error('Could not find asset for versioning' + filename)
        }

        return mixManifest[filename]
    })

    View.global('versioncss', (filename) => {
        filename = `/css/${filename}.css`
        if (!mixManifest.hasOwnProperty(filename)) {
            throw new Error('Could not find asset for versioning' + filename)
        }

        return mixManifest[filename]
    })
})
```


This will create two global methods we can use in our view. Go to 
 `resources/assets/views/app.edge` and replace

```HTML
{{ script('/js/main.js') }}
```

with

```HTML
{{ script(versionjs('main')) }}
```

And that's all there is to cache busting.

#### Deployment

There is already [an article](https://scotch.io/tutorials/deploying-adonisjs-apps-to-heroku) on deploying Adonis apps to Heroku. Because we are having our assets on the same project though, we have to add one or two things to make the deployment run smoothly. Add the following code under `scripts` inside `package.json`

```json
// package.json

"heroku-postbuild": "npm run assets-production"
```

This tells Heroku to transpile our assets during deployment. If you are not using Heroku, other services probably offer similar solutions.

***In case the deployment fails...***

You might have to configure your Heroku app to also install dev dependencies. You can configure it by executing the following command

```bash
heroku config:set NPM_CONFIG_PRODUCTION=false YARN_PRODUCTION=false
```

Alternatively you can set the configurations on the Heroku website directly.





And that's all there is to it.

To skip all the setting up you can simply clone the demo repo with

```bash
adonis new application-name --blueprint=MZanggl/adonis-vue-demo
```

Let me know if you are interested in a blueprint that already includes registration routes and controllers, vuetify layout, vue store etc.

---

If this article helped you, I have a lot more tips on simplifying writing software [here](https://michaelzanggl.gumroad.com/l/intent-driven-development).