Routes, Controllers and Views

In the previous chapter, we created the Post and Comment models with their database tables and relationships. Now we'll bring those models to life by building pages where users can actually see posts.

Note

This tutorial covers basic routing, controllers, and views. For advanced topics like route groups, middleware, named routes, route parameters validation, and Edge template components, see the Routing guide, Controllers guide, and Edge documentation.

Overview

Right now, your posts and comments exist only in the database. Let's build two pages: one that lists all posts, and another that shows a single post with its comments.

This is where you'll see the complete MVC (Model-View-Controller) pattern in action — models handle data, controllers coordinate logic, and views display everything to users.

Before we begin, make sure your development server is running.

node ace serve --hmr

Displaying the posts list

Let's build the complete feature for displaying a list of posts. We'll create a controller, add a method to fetch posts, register a route, and create the view template.

  1. Creating the controller

    Start by creating a controller to handle posts-related requests. Run this command.

    node ace make:controller posts

    This creates a new file at app/controllers/posts_controller.ts. Open it up and you'll see a basic controller class. Let's add a method to list all posts.

    app/controllers/posts_controller.ts
    import Post from '#models/post'
    import { type HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      async index({ view }: HttpContext) {
        const posts = await Post
          .query()
          .preload('user')
          .orderBy('createdAt', 'desc')
    
        return view.render('posts/index', { posts })
      }
    }

    A few things to note here:

    • We're preloading the user relationship so we can display the author's name without extra queries
    • We're ordering posts by creation date with newest first
    • And passing the posts to a view template called posts/index.
  2. Defining the route

    Open your routes file and register a route.

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { controllers } from '#generated/controllers'
    
    router.on('/').render('pages/home').as('home')
    router.get('/posts', [controllers.Posts, 'index'])

    The route definition connects the /posts URL to your controller's index method. When someone visits /posts, AdonisJS will call PostsController.index() and return whatever that method returns.

    Note

    The #generated/controllers import is automatically generated by AdonisJS and provides type-safe references to your controllers. For more details on how this works, see the Controllers guide.

  3. Creating the view template

    Time to create the view template.

    node ace make:view posts/index

    This creates a new file at resources/views/posts/index.edge. Open it and add the following code inside it.

    resources/views/posts/index.edge
    @layout()
      <div class="container">
        <div class="posts-list-title">
          <h1> Posts </h1>
        </div>
    
        @each(post in posts)
          <div class="post-item">
            <h2> {{ post.title }} </h2>
    
            <div class="post-meta">
              <div>By {{ post.user.fullName }}</div>
    
              <span>.</span>
              <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>
    
              <span>.</span>
              <div><a href="/posts/{{ post.id }}"> View comments </a></div>
            </div>
          </div>
        @end
      </div>
    @end

    This template uses the existing layout component that came with your starter kit. The layout handles the basic HTML structure, and you provide the main content by wrapping it in @layout tag.

    Inside, we loop through each post with @each and display its title, the author's name, and a link to view post comments.

Visit /posts and you should see a list of all your posts!

Displaying a single post

Now let's add the ability to view an individual post with its details. We'll implement the controller method, register the route with a dynamic parameter, and create the view template.

  1. Implementing the controller method

    Add the show method to your controller.

    app/controllers/posts_controller.ts
    import Post from '#models/post'
    import type { HttpContext } from '@adonisjs/core/http'
    
    export default class PostsController {
      async index({ view }: HttpContext) {
        const posts = await Post
          .query()
          .preload('user')
          .orderBy('createdAt', 'desc')
    
        return view.render('posts/index', { posts })
      }
    
      async show({ params, view }: HttpContext) {
        const post = await Post
          .query()
          .where('id', params.id)
          .preload('user')
          .firstOrFail()
    
        return view.render('posts/show', { post })
      }
    }

    We're using firstOrFail() here, which will automatically throw a 404 error if no post exists with that ID. No need to manually check if the post exists—AdonisJS handles that for you.

  2. Registering the route

    Now let's register the route for this controller method.

    start/routes.ts
    import router from '@adonisjs/core/services/router'
    import { controllers } from '#generated/controllers'
    
    router.get('/posts', [controllers.Posts, 'index'])
    router.get('/posts/:id', [controllers.Posts, 'show'])
    • The :id part is a route parameter.
    • When someone visits /posts/5, AdonisJS captures that 5 and makes it available in your controller as params.id.
    • You can name the parameter anything you want, :id, :postId, :slug — just be consistent when accessing it.
  3. Creating the view template

    Create the view template for displaying a single post.

    node ace make:view posts/show

    This creates resources/views/posts/show.edge. Open it and add the following code.

    resources/views/posts/show.edge
    @layout()
      <div class="container">
        <div>
          <h1>
            {{ post.title }}
          </h1>
        </div>
    
        <div class="post">
          <div class="post-meta">
            <div>By {{ post.user.fullName }}</div>
    
            <span>.</span>
            <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>
          </div>
    
          <div class="post-summary">
            {{ post.summary }}
          </div>
        </div>
      </div>
    @end

    Try clicking on a post from your list page. You should now see the full post with its title, author, and content.

Using named routes

Right now, we're hardcoding URLs like /posts/{{ post.id }} in our templates. This works, but what happens if we decide to change our URL pattern from /posts/:id to /showcase/:id? We'd have to find and replace every hardcoded URL throughout our application.

This is where named routes come in. Named routes let you assign a unique name to each route, then reference that name when building URLs. If the URL pattern changes, you only update it in one place (the route definition), and all your links automatically work with the new pattern.

When you use a controller in your route definition, AdonisJS automatically generates a route name based on the controller and method names. For example:

  • The [controllers.Posts, 'index'] gets the name posts.index.
  • And [controllers.Posts, 'show'] gets the name posts.show.

You can also manually name routes using the .as() method, which is useful for routes that don't use controllers or when you want a custom name.

Named routes can be referenced across your entire application:

  • In templates using the urlFor() helper
  • In the @form component's route parameter
  • In the @link component's route parameter
  • In controllers using response.redirect().toRoute()
  • And more

For the complete guide on URL building and named routes, see the URL builder documentation.

Let's update our posts listing page to use named routes.

resources/views/posts/index.edge
@layout()
  <div class="container">
    <div class="posts-list-title">
      <h1> Posts </h1>
    </div>

    @each(post in posts)
      <div class="post-item">
        <h2> {{ post.title }} </h2>

        <div class="post-meta">
          <div>By {{ post.user.fullName }}</div>

          <span>.</span>
          <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>

          <span>.</span>
          <div><a href="/posts/{{ post.id }}"> View comments </a></div>
          <div><a href="{{ urlFor('posts.show', [post.id]) }}"> View comments </a></div>
        </div>
      </div>
    @end
  </div>
@end

Adding comments to the post view

Finally, let's display the comments for each post. First, we need to preload the comments and their authors in the controller.

app/controllers/posts_controller.ts
import Post from '#models/post'
import { HttpContext } from '@adonisjs/core/http'

export default class PostsController {
  async index({ view }: HttpContext) {
    const posts = await Post.query().preload('user').orderBy('createdAt', 'desc')

    return view.render('posts/index', { posts })
  }

  async show({ params, view }: HttpContext) {
    const post = await Post.query()
      .where('id', params.id)
      .preload('user')
      .preload('comments', (query) => {
        query.preload('user').orderBy('createdAt', 'asc')
      })
      .firstOrFail()

    return view.render('posts/show', { post })
  }
}

We're preloading comments along with each comment's user (the author), and ordering them by creation date with oldest first. Now update the view to display them.

resources/views/posts/show.edge
@layout()
  <div class="container">
    <div>
      <h1>
        {{ post.title }}
      </h1>
    </div>

    <div class="post">
      <div class="post-meta">
        <div>By {{ post.user.fullName }}</div>

        <span>.</span>
        <div><a href="{{ post.url }}" target="_blank">{{post.url}}</a></div>
      </div>

      <div class="post-summary">
        {{ post.summary }}
      </div>

      <div class="post-comments">
        <h2>
          Comments
        </h2>

        @each(comment in post.comments)
          <div class="comment-item">
            <p> {{ comment.content }} </p>
            <div class="comment-meta">
              By {{ comment.user.fullName }} on {{ comment.createdAt.toFormat('MMM dd, yyyy') }}
            </div>
          </div>
        @else
          <p> No comments yet. </p>
        @end
      </div>
    </div>
  </div>
@end

Refresh your post detail page and you'll now see all the comments listed below the post content!

What you've built

You've just completed the full MVC flow in AdonisJS:

  • Routes that map URLs to controller actions
  • Controllers that fetch data from your models and pass it to views
  • Views that display data using Edge templates
  • Relationships that let you eager load related data efficiently
  • Named routes that make your templates maintainable