Upgrading from 3.x

AdonisJs 4.0, known as (dawn), is a major release of the framework after more than a year. Each major release comes with a handful of breaking changes, which is required to keep the framework fresh and address all issues reported during the year.

Javascript movement

This release is very special since much movement has happened in the ES6 world, and Node.js 8.0 has implemented 99% of the language features. Also, the V8 engine has received lots of speed improvements.

The time has finally come to update the code base and start supporting the latest features so that we can all write simpler and more expressive code while taking advantage of all the performance benefits.

Language changes

Below is the list of changes made due to the availability of ES6 features.

Using async/await

Generator functions using the yield keyword are no longer supported, and instead, we make use of async/await.

This means all functions starting with * need to be replaced with async, and yield keywords need to be replaced with await.

Old way

* index () {
  yield User.all()
}

New way

async index () {
  await User.all()
}

Destructuring

ES6 allows object destructuring which can be used to pull selected values from an object.

In the newer version of AdonisJs, all route handlers and controller actions receive an object of values instead of receiving multiple parameters with request and response.

Old Way

Route.get('/', function * (request, response) {

})

New Way

Route.get('/', async ({ request, response }) => {

})

Directory structure

The directory structure has been tweaked a bit to keep it as flat as possible and also to make the upgrade path easier in upcoming versions.

bootstrap → start

The bootstrap directory has been renamed to start, which performs the same function but is easier to understand.

Also, a bunch of files like http.js and kernel.js have been removed since they contained the code to wire the app and boot the server. All this has been moved to a separate package called @adonisjs/ignitor.

Controllers

The location of Controllers have been changed as follows

app/Http/Controllers → app/Controllers/Http

Having a top level Http directory does not make sense, since the only thing we store inside it is the Controllers, so it is better to have Controllers as the top-level directory and keep WebSocket and Http controllers inside it.

Models

The models' directory has been pluralized.

app/Model → app/Models

routes.js

The routes.js file has been moved from app/Http/routes.js to start/routes.js. Since routes are the starting point to your app, they should be in the start directory.

kernel.js

The kernel.js file, which stores the list of Middleware, has been moved from app/Http/kernel.js to start/kernel.js.

storage → tmp

The job of the storage directory was to hold files which are never committed to version control like Github. However, the name itself did not make it clear that the directory holds temporary files. The rename to tmp makes it more clear.

Routing

addCollection, addMember

The addCollection and addMember methods have been removed from the resource routing. This is done in favor of keeping routes straightforward since it was never clear in which sequence a new member was added via the addMember method. For example

Route
  .resource('users', 'UserController')
  .addMember('active')

The resource creates a route called users/:id as part of the resourceful routes and adding a new member adds the users/active route.

It is unclear which action gets called first, since users/:id will catch all the routes including users/active.

Instead, you should manually define the other routes.

Route.get('users/active', 'UserController.getActive')
Route.resource('users', 'UserController')

group

The group method signature has been changed, and now it is optional to pass the name.

Earlier

Route.group('auth', () => {
})

Now

Route.group(() => {
})

The 1st param is the group name, which is optional now. If you give a group name, then all route names are prefixed with that.

Route.route

The Route.route method signature has been tweaked a bit.

Earlier

Route.route('/', ['GET', 'POST'], () => {
})

Now

Route.route('/', () => {
}, ['GET', 'POST'])

Request

Below is the list of breaking API changes in the Request object.

param/params

The param/params methods have been removed in 4.0, and instead, a separate object is passed with all the route params.

Earlier

Route.get('users/:id', function (request) {
  const id = request.param('id')
})

Now

Route.get('users/:id', function ({ params }) {
  const id = params.id
})

Views

The view layer of AdonisJs now uses Edge over nunjucks which is a home-grown template engine written for AdonisJs itself.

Extending the core of nunjucks was so painful that adding new tags and helpers was becoming hard. Edge has a very minimal developer API, and it is pretty straightforward to extend.

Make sure to check edge guides to learn more about it.

response.sendView

The response.sendView function has been removed and instead a view instance is passed to all the HTTP requests.

Earlier

Route.get('/', function * (request, response) {
  yield response.sendView('home')
})

Now

Route.get('/',  ({ view }) => {
  return view.render('home')
})

Auth

The authentication engine has remained about the same. This section outlines some of the breaking changes.

request.auth

The request.auth method has been removed and instead a dedicated auth instance is passed to all HTTP requests.

Earlier

Route.get('/', function * (request) {
  const auth = request.auth
  console.log(auth.currentUser)
})

Now

Route.get('/', ({ auth }) => {
  console.log(auth.user)
})

revokeToken(s)

The api authenticator used to have revokeToken and revokeTokens methods, which have been removed and instead you can use the User model directly to revoke tokens.

Earlier

Route.get('/', function (request) {
  yield request.auth.revokeTokens(request.currentUser)
})

Now

Route.get('/', async ({ auth }) => {
  await auth.user
    .tokens()
    .where('type', 'api_token')
    .update({ is_revoked: true })
})

Since the tokens table now keep all sort of tokens like remember tokens and jwt refresh tokens, it is more convenient to use the User model directly and revoke the required tokens.

Models

A bunch of changes had been made to lucid, the majority of which are improvements, bug fixes, and much-awaited features.

Here is the list of breaking changes.

extend

All models used to fetch the Lucid namespace to extend themselves. Going forward, you need to pull the Model namespace.

Earlier

const Lucid = use('Lucid')

class User extends Lucid {
}

Now

const Model = use('Model')

class User extends Model {
}

dateFormat

The dateFormat getter has been removed in favor of the alternate approach to handling dates. Read this blog post to learn more about it.

useTransaction

To run model operations inside a transaction, the useTransaction method was used. In 4.0, you pass the transaction object directly to the save and create methods.

Earlier

const trx = yield Database.beginTransaction()

const user = new User()
user.username = 'virk'
user.useTransaction(trx)

yield user.save()

Now

const trx = await Database.beginTransaction()

const user = new User()
user.username = 'virk'

await user.save(trx)

belongsToMany

The belongsToMany method used to receive the pivot table name as part of the method call which has been changed in 4.0.

Earlier

class User extends Lucid {
  cars () {
    return this.belongsToMany('App/Model/Car', 'my_cars')
  }
}

Now

class User extends Model {
  cars () {
    return this
      .belongsToMany('App/Models/Car')
      .pivotTable('my_cars')
  }
}

with

The with method is used to eagerload relationships, and the signature has been changed quite a bit.

Earlier

User
  .query()
  .with('profiles', 'posts')

Now

User
  .query()
  .with('profiles')
  .with('posts')

Now you have to call with multiple times to eagerload multiple relations. This is done since the 2nd param to the with method is a callback to add query constraints on the relationship.

Earlier

User
  .query()
  .with('profiles')
  .scope('profiles', (builder) => {
    builder.where('is_latest', true)
  })

Now

User
  .query()
  .with('profiles', (builder) => {
    builder.where('is_latest', true)
  })

attach

The attach method of Belongs To Many relationship has been changed in how it receives the values for pivot tables.

Earlier

await user.cars().attach([1], { current_owner: true })

Now

await user.cars().attach([1], (pivotModel) => {
  pivotModel.current_owner = true
})

The new signature makes it easier to add conditional attributes. For example: When calling attach with 3 cars and wanting to set a different current_owner attribute for each car. The callback approach makes it easier to do that since the callback is invoked for cars.length number of times.

Factories

The database factories API is tweaked a little to make it more explicit.

create

Earlier, the create method could create one or many rows based upon the number passed to it. Now you have to call createMany to create multiple rows.

It makes sure that the create method always returns the created model instance and createMany always returns an array of created model instances.

Earlier

// create one
Factory.model('App/Models/User').create()

// create many
Factory.model('App/Models/User').create(3)

Now

// create one
Factory.model('App/Models/User').create()

// create many
Factory.model('App/Models/User').createMany(3)

make

The make method has been changed accordingly, and the makeMany method has been introduced.

Sessions

The way to interact with sessions has been changed too. All of the sessions related code has been extracted from core to an individual repo. Now API only servers can easily remove sessions from their app.

Using sessions

Earlier

Route.get('/', function * (request) {
  yield request.session.put('username', 'virk')
})

Now

Route.get('/', ({ session }) => {
  session.put('username', 'virk')
})

Flash messages

The flash messages signature has also been changed

Earlier

Route.get('/', function * (request) {
  yield request.withAll().flash()

  // errors
  yield request
    .withAll()
    .andWith({ error: { message: 'Some error' } })
    .flash()
})

Now

Route.get('/', ({ session }) => {
  session.flashAll()

  // errors
  await session
    .withErrors({ message: 'Some error' })
    .flashAll()
})