Playing Next Lesson In
seconds

Let's Learn AdonisJS 7 #2.0

Defining Routes & Rendering Views

In This Lesson

Learn how to define routes and render views in AdonisJS. Understand route definitions, rendering views, and the router service.

Created by
@tomgobich
Published

Routes are defined within the preload stage of our application's lifecycle, meaning we can find them within our start directory, specifically within the routes.ts file.

The Hypermedia Starter Kit gave us a few predefined routes.

/*
|--------------------------------------------------------------------------
| Routes file
|--------------------------------------------------------------------------
|
| The routes file is used for defining the HTTP routes.
|
*/

import { controllers } from '#generated/controllers'
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'

router.on('/').render('pages/home').as('home')

router
  .group(() => {
    router.get('signup', [controllers.NewAccount, 'create'])
    router.post('signup', [controllers.NewAccount, 'store'])

    router.get('login', [controllers.Session, 'create'])
    router.post('login', [controllers.Session, 'store'])
  })
  .use(middleware.guest())

router
  .group(() => {
    router.post('logout', [controllers.Session, 'destroy'])
  })
  .use(middleware.auth())
Copied!
  • start
  • routes.ts

First, it's importing the router container service. This is a singleton of AdonisJS's router, meaning the same single instance of this service is used every time it's imported for the duration of our server's life.

Several AdonisJS batteries export service containers for convenience, and can be noted by the /services/ import path.

With this router, we can define route definitions. Route definitions are paths we define for our application to handle. For example, when we booted our application in lesson 1.2, we saw a landing page. This is defined by the router.on('/') definition. The on method is a shorthand syntax that will define a GET route definition for the pattern provided with a few simple handling options, like rendering a page as it's doing here.

  router
    .on('/')  // defines GET: / route definition for the pattern '/'
    .render('pages/home') // handles that route by rendering the home page
    .as('home') // names the route (more on this later)
Copied!
  • start
  • routes.ts

In addition to the on shorthand method, each HTTP Verb has its own method that allows us fine-grain control over how to handle the route. Each HTTP Verb has a purpose.

  • GET is for rendering pages and fetching information

  • POST is for creating records and sending general payloads

  • PUT is for updating a record (multiple properties)

  • PATCH is for a targeted record update (singular properties)

  • DELETE is for deleting records

Defining a Route

Let's start by creating a page of our own to show our application's terms of use, a page almost every application has.

router.on('/terms').render('pages/terms')
Copied!

With this, we're saying that when we request the /terms pattern, we should render the page within resources/views/pages/terms.edge. When rendering, AdonisJS will automatically look within resources/views so we only need to specify the path from there. Now, we haven't created this page yet, so when we request this route, we'll be met with an error screen.

This error screen is Youch, an informative and developer-friendly error screen to tell us exactly what error we got and a stack trace to help track it down. This screen will only be displayed when our application is in development mode.

Error: Cannot resolve "/../lets-learn-adonisjs-7/resources/views/pages/terms.edge". Make sure the file exists

In this case, it's telling us it cannot resolve the page we've told it to render. Let's fix this by creating this page!

Creating Views

AdonisJS has its own templating engine called EdgeJS, which uses .edge as its file extension and the AdonisJS Extension Pack we installed in Module 1 adds support for EdgeJS to Visual Studio Code.

touch resources/views/pages/terms.edge
Copied!

Then we can use emmet to quickly stub HTML5 markup with html:5 to get:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>
Copied!
  • resources
  • views
  • pages
  • terms.edge

On our body, let's just add some simple text!

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>Terms of Service</h1>
  <p>This is the terms of service page.</p>
</body>
</html>
Copied!
  • resources
  • views
  • pages
  • terms.edge

Now, if we refresh our browser, instead of being met by Youch with an error screen, we see our page!

Let's add another view, this time, using the Ace CLI!

node ace make:view pages/challenges/index
# DONE:    create resources/views/pages/challenges/index.edge
Copied!

Our naming here of challenges/index follows resourceful naming conventions, where "challenge" is our resource. Resourceful naming has conventions for each HTTP operation needed to list, show, create, update, and delete a resource.

  • GET /challenges uses the name index and renders a list of items for the resource

  • GET /challenges/1 uses the name show and renders a single item, "1" here serves as an identifier.

  • GET /challenges/create uses the name create and renders a form to create an item

  • POST /challenges uses the name store and handles the form to create an item

  • GET /challenges/1/edit uses the name edit and renders a form to edit an item

  • PUT /challenges/1 uses the name update and handles the form to update an item

  • DELETE /challenges/1 uses the name destroy and handles the deletion of an item

We'll see this naming convention used in both our route definitions and view names. Keeping to the convention helps keep things predictable and easily understandable.

Okay, let's add some simple HTML5 markup again, using html:5, with some basic text noting which page we're on.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>Challenges</h1>
</body>
</html>
Copied!
  • resources
  • views
  • pages
  • challenges
  • index.edge

Rendering Views

Finally, let's define a route for this view so we can actually request and render it. This time, we'll use the get method. Unlike the on shorthand, this accepts a second argument allowing us fine-grained control over how the route should be handled via a route handler.

router.get('/challenges', async (ctx) => {
  
})
Copied!
  • start
  • routes.ts

The route handler is provided an HttpContext, contextual information about the request. It's with this that we can specify what page to render, gain access to request and response properties, and a lot more. As we continue through this series, we'll get more acquainted with the HttpContext, for now though, let's use it to render our page.

router.get('/challenges', async (ctx) => {
  return ctx.view.render('pages/challenges/index')
})
Copied!
  • start
  • routes.ts

Our render method is chained off the view property within our HttpContext. This method is the same render method we used with our shorthand; however, this time you'll notice we get some convenient autocomplete options pulling from pre-existing pages within our application.

Okay, great with that saved, we should now be able to head to /challenges in our browser to see this page rendered out!

Before we move on, in case you're concerned that we have to manually refresh our browser to see our changes, don't worry! This is simply because our browser isn't hooked into our Vite dev server. We can fix this by applying our starter kit's layout component to the page. This layout comes pre-packaged with the HTML:5 boilerplate we already have on our page in addition to an @vite tag pointing to our CSS and JS file. It's this @vite tag that wires our browser up to our dev server to get our browser to automatically pick up changes we make in-code. There's a lot going on here; we'll slowly get comfortable with all of this. For now, we can swap our HTML5 boilerplate with our layout.

@layout()

  <h1>Challenges</h1>

@end
Copied!
  • resources
  • views
  • pages
  • challenges
  • index.edge

Anything in between the layout start and end tag is ultimately rendered out via the layout's main slot, again... more on that later!

<!DOCTYPE html>
<html lang="en-us">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>
      AdonisJS - A fully featured web framework for Node.js
    </title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    @stack('dumper')
  </head>
  <body>
    @include('partials/header')
    <main>
      @include('partials/flash_alerts')
      {{{ await $slots.main() }}}
    </main>
  </body>
</html>
Copied!
  • resources
  • views
  • components
  • layout.edge

So, if we add a paragraph to our challenges page...

@layout()

  <div>
    <h1>Challenges</h1>
    <p>Welcome to the challenges page.</p>
  </div>

@end
Copied!
  • resources
  • views
  • pages
  • challenges
  • index.edge

We'll see this automatically applied in our browser without the need to refresh manually!

Join the Discussion 1 comments

Create a free account to join in on the discussion
  1. i loved this part man i really loved it

    0