DEV Community

Cover image for Getting started with Adonis, Vue & Inertia
punnyprogrammer
punnyprogrammer

Posted on

Getting started with Adonis, Vue & Inertia

AdonisJS is a node framework written in TypeScript that (for lack of a better description) appears to be a clone of PHP's Laravel. 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. (Enjoy!)

Overview

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

  • AdonisJS: The Server
  • VueJS: The Client
  • Laravel Mix: Asset Compiler. Easy to use and is optimized to work with Vue
  • InertiaJS: 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 Adonis, Vue, Inertia Starter.

yarn create adonis-ts-app a-v-i-s
Enter fullscreen mode Exit fullscreen mode

When prompted, select the web project structure. Outside of that, all of the default's are what we want.

Screenshot of AdonisJS initiation screen

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 or Prisma 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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)
  }
}

Enter fullscreen mode Exit fullscreen mode

User Model

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
}

Enter fullscreen mode Exit fullscreen mode

Once our Migration is setup, we can run it.

node ace migration:run
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

node ace configure @eidellev/inertia-adonisjs
Enter fullscreen mode Exit fullscreen mode

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

<!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>

Enter fullscreen mode Exit fullscreen mode

Now we can open start/kernal.ts and ensure our Inertial Middleware is registered

Server.middleware.register([
  () => import('@ioc:Adonis/Core/BodyParser'),
  () => import('@ioc:EidelLev/Inertia/Middleware'),
]);
Enter fullscreen mode Exit fullscreen mode

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

<template>
  <div>
    Hello <a href="/world">World</a>!
  </div>
</template>

Enter fullscreen mode Exit fullscreen mode

/resources/vue/Pages/World.vue

<template>
  <div>
    <a href="/">Hello</a> World!
  </div>
</template>

Enter fullscreen mode Exit fullscreen mode

Routes

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

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

Route.get('/world', async ({inertia}) => {
  return inertia.render('World')
})
Enter fullscreen mode Exit fullscreen mode

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 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.

//...
"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
    }
  ],
//...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's create a .bablerc file as follow:

{
  "plugins": ["@babel/plugin-syntax-dynamic-import"]
}
Enter fullscreen mode Exit fullscreen mode

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

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();

Enter fullscreen mode Exit fullscreen mode

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.

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);
  },
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

SERVER: Terminal

node ace serve ---watch
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (1)

Collapse
sahil0571 profile image
sahil0571

Can you show passing controller data from it ?