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 existsIn 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.edgeCopied!
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.edgeCopied!
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 /challengesuses the nameindexand renders a list of items for the resourceGET /challenges/1uses the nameshowand renders a single item, "1" here serves as an identifier.GET /challenges/createuses the namecreateand renders a form to create an itemPOST /challengesuses the namestoreand handles the form to create an itemGET /challenges/1/edituses the nameeditand renders a form to edit an itemPUT /challenges/1uses the nameupdateand handles the form to update an itemDELETE /challenges/1uses the namedestroyand 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> @endCopied!
- 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> @endCopied!
- resources
- views
- pages
- challenges
- index.edge
We'll see this automatically applied in our browser without the need to refresh manually!