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 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.
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
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
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
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:
<!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
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
<template>
<div>
Hello <a href="/world">World</a>!
</div>
</template>
/resources/vue/Pages/World.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
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 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
}
],
//...
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:
{
"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.
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.
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!
Top comments (1)
Can you show passing controller data from it ?