Getting started with Adonis, Vue & Inertia
Tags: vue, node
Published: 2022-01-22 23:44:24

**AdonisJS** is a node framework written in TypeScript that (for lack of a better description) appears to be a clone of PHP's [Laravel](https://laravel.com/). As someone who spent years developing in Laravel before moving to JavaScript, I find AdonisJS to be a welcome addition to my stack. 

**InertiaJS** bills itself as "JavaScript Apps for the Modern Monolith". It is an amazing utility that "glues" your server-side and client-side together automatically allowing you to have your SPA while only having to write all API's and definitions once. 

## Wanna skip to the end? 
If you don't need all the step by step and instead just want to see the finished code base I've got you covered. You can see the [sample repo this code creates here](https://github.com/punnyprogrammer/a-v-i-s). (Enjoy!)

## Overview
When done, we should have a small working starter app using the following stack:

- [AdonisJS](https://adonisjs.com/): The Server
- [VueJS](https://vuejs.org/): The Client
- [Laravel Mix](https://laravel-mix.com/): Asset Compiler. Easy to use and is optimized to work with Vue
- [InertiaJS](https://inertiajs.com/): Communication Layer between Server & Client


## Assumptions 
While just about anyone can follow the step by step and should be able to achieve success, I am assuming you are familiar with all of the tools listed here. This is not an introductory to these tools, it's just a quick tutorial on how to wire them together. 

**NOTE:** You'll see I use `yarn` below, however you are welcome to replace the yarn commands with the appropriate `npm` command if you prefer.

## Server Side Setup
First up, we'll tackle the server side of our project. Overall, we'll tackle the following: 
- AdonisJS
- Database
- Authentication
- Inertia Server Side

Once that's done we can move onto the Client side setup.

### AdonisJS
Let's create a new AdonisJS project for `A`donis, `V`ue, `I`nertia `S`tarter.
```
yarn create adonis-ts-app a-v-i-s
```
When prompted, select the `web` project structure. Outside of that, all of the default's are what we want. 

![Screenshot of AdonisJS initiation screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o07spv3r8b7y2qp0b4cg.png)

Once creation is done, we'll jump into the project with `cd a-v-i-s` so we can continue our setup.

### Database
Lucid is AdonisJS Built-In ORM. It's extremely powerful and works much like Laravel's Eloquent ORM. While Lucid is built in, you can optionally use [Sequelize](https://sequelize.org/) or [Prisma](https://www.prisma.io/) with AdonisJS as well.

I'll be using `MySQL`, so below I'll also include the necessary `mysql2` package. However, feel free to use whichever DB driver works best for you. 

```
yarn add @adonisjs/lucid mysql2
node ace configure @adonisjs/lucid
```

Once done, update your `.env` and your `env.ts` files accordingly. 

### Authentication 
Most starter apps want quick access to user authentication, so we'll add that one layer to our starter app. AdonisJS has `bouncer` that does a great job. 

```
yarn add @adonisjs/bouncer
node ace configure @adonisjs/bouncer
```

#### User Model & Migration
We'll need a User Model we can eventually authenticate against. I'm going to create the User Model and we'll add the `-m` flag to create a matching database migration file. 

```
node ace make:model user -m
```

Next I'll set up some standard columns I typically use in most of my User tables. 

**_Note_**: that I use `MySQL 8.013` which supports some features previous versions do not. If you are using a version `MySQL <= 8.012`, you'll want to uncomment out some of the commented code in my examples.

**`User Migration`**

```ts
import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class Users extends BaseSchema {
  protected tableName = 'users'

  public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('email').unique().notNullable()
      table.string('name').notNullable()
      table.dateTime('email_verified_at').nullable()
      table.string('password').nullable()
      table.string('reset_token').nullable().unique()
      table.dateTime('reset_token_expires', { useTz: true}).nullable()

      // MySQL >= 8.013
      table.string('password_salt').unique().notNullable().defaultTo('MD5(RAND())')
      // MySQL < 8.013
      // table.string('password_salt').unique()

      // MySQL >= 8.013
      table.string('uuid').unique().defaultTo('UUID()')
      // MySQL < 8.013
      // table.string('uuid').unique()

      table.timestamp('created_at')
      table.timestamp('updated_at')
    })
  }

  public async down () {
    this.schema.dropTable(this.tableName)
  }
}

```

**`User Model`**

```ts
import { DateTime } from 'luxon'
import { BaseModel, beforeUpdate, column } from '@ioc:Adonis/Lucid/Orm'
// import { BaseModel, beforeCreate, beforeUpdate, column } from '@ioc:Adonis/Lucid/Orm'
import Hash from '@ioc:Adonis/Core/Hash'
// import uuid from "uuid"

export default class User extends BaseModel {

  // If using MySQL <= 8.012
  // @beforeCreate()
  // public static async generateUuid(user: User) {
  //   user.uuid = uuid.v4()
  // }
  // public static async generatePasswordSalt(user: User) {
  //     user.passwordSalt = await Hash.make(uuid.v4())
  // }

  @beforeUpdate()
  public static async hashPassword(user: User) {
    if( user.$dirty.password) {
      user.password = await Hash.make(user.password)
    }
  }

  @column({ isPrimary: true })
  public id: number

  @column()
  public email: string

  @column()
  public name: string

  @column.date()
  public emailVerifiedAt: DateTime

  @column({ serializeAs: null})
  public password: string

  @column({ serializeAs: null})
  public passwordSalt: string

  @column()
  public resetToken: string

  @column.date()
  public resetTokenExpires: DateTime

  @column()
  public uuid: string

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}

```

Once our Migration is setup, we can run it. 

```
node ace migration:run
```
### Inertia (Server Side)
Inertia requires both a server side and a client side setup. Since we're doing the server work now, we'll go ahead and get Inertia's server side setup. 

```
yarn add @inertiajs/inertia @inertiajs/inertia-vue3 @eidellev/inertia-adonisjs vue@3
```

Now we can connect the server with Inertia. Feel free to use the default settings.

```
node ace configure @eidellev/inertia-adonisjs
```

When done, you should have a file at `resources/views/app.edge`. Open the file and replace or modify it to match as follows:

```html
<!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" />
</head>

<body>
  @inertia
  <script src="{{ mix('/scripts/main.js') }}"></script>
</body>
</html>

```

Now we can open `start/kernal.ts` and ensure our Inertial Middleware is registered
```ts
Server.middleware.register([
  () => import('@ioc:Adonis/Core/BodyParser'),
  () => import('@ioc:EidelLev/Inertia/Middleware'),
]);
```

#### Views (aka Vue Pages)
Inertia is going to serve our Vue files from the server to our front end for us. So we just need to create some Vue files for our inertia adapter to use. Create the following two files: 

**`/resources/vue/Pages/Hello.vue`**
```vue
<template>
  <div>
    Hello <a href="/world">World</a>!
  </div>
</template>

```
**`/resources/vue/Pages/World.vue`**
```vue
<template>
  <div>
    <a href="/">Hello</a> World!
  </div>
</template>

```

#### Routes
The last part of our server is to setup our routes to return our Vue files. Update `start/routes.ts` as follows

```ts
Route.get('/', async ({ inertia }) => {
  return inertia.render('Hello')
})

Route.get('/world', async ({inertia}) => {
  return inertia.render('World')
})
```

## Client Side Setup
Now that the server is setup, we can configure the client side of our application. All we need to do is bring in Laravel Mix, which will handle all asset compiling, and then create our entrypoint.

### Laravel Mix (Asset Compiling)
Laravel Mix is an amazing front end compiler that plays very well with Vue. We'll also be leveraging the [adonis-mix-asset](https://www.npmjs.com/package/adonis-mix-asset) package. This package will allow us to have additional `ace` commands such as `mix:watch` and `mix:build`

First, we want to ensure our `.adonisrc.json` file is updated to serve our static assets. Open the file and ensure your `metaFiles` looks similar to this.

```json
//...
"metaFiles": [
    ".adonisrc.json",
    {
      "pattern": "resources/views/**/*.edge",
      "reloadServer": true
    },
    {
      "pattern": "public/scss/*.css",
      "reloadServer": false
    },
    {
      "pattern": "public/scripts/*.js",
      "reloadServer": false
    },
    {
      "pattern": "public/**",
      "reloadServer": false
    }
  ],
//...
```

Once that's done, we can install and configure laravel-mix.

```
yarn add adonis-mix-asset @babel/plugin-syntax-dynamic-import
yarn add --dev  vue-loader@^16.8.3 autoprefixer postcss resolve-url-loader laravel-mix@next

node ace invoke adonis-mix-asset
```

Let's create a `.bablerc` file as follow:
```json
{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
```

You'll find a new `webpack.mix.js` file in your project. Add the following code to that file. 

```js
const mix = require('laravel-mix')
const path = require("path");
mix.setPublicPath('public')

mix
  .js("resources/scripts/main.js", path.resolve(__dirname, "public/scripts"))
  .webpackConfig({
    context: __dirname,
    node: {
      __filename: true,
      __dirname: true,
    },
    resolve: {
      alias: {
        "@": path.resolve(__dirname, "resources/vue"),
        "~": path.resolve(__dirname, "resources/vue"),
      },
    },
  })
  .options({
    processCssUrls: true,
  })
  .vue()
  .version();

```

### App Entrypoint
 
We've told our app the entrypoint is `resources/scripts/main.js`, so we need to create that file and we're all set. 

```js
import { createApp, h } from "vue";
import { createInertiaApp } from "@inertiajs/inertia-vue3";

createInertiaApp({
  resolve: (name) => import(`../vue/Pages/${name}`),
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .mount(el);
  },
});
```



### Putting it all together
Your starter app should be ready to go. Now we just fire off the server and client commands. 

**`ASSET COMPILING: Terminal`**
```
node ace mix:watch
```

**`SERVER: Terminal`**
```
node ace serve ---watch
```

When your build is complete, your terminal should provide you a URL. You should see a simple "Hello World" message at the site root. Clicking on "World" should take you to a new page allowing you to click back on "Hello". This is Inertia serving Vue files from the Server without having to write client side routing or logic. 

I hope you enjoy!