Skip to content
Aurelia Framework Explained — Complete Developer's Guide

Aurelia Framework Explained — Complete Developer's Guide

DodaTech Updated Jun 6, 2026 8 min read

Aurelia is a JavaScript framework for building browser, mobile, and desktop applications that uses conventions over configuration, providing powerful data binding, dependency injection, and routing with clean, readable code.

What You’ll Learn

  • How Aurelia’s conventions-based approach reduces boilerplate compared to other frameworks
  • How dependency injection works and why it makes testing easier
  • How to use Aurelia’s data binding system (one-way, two-way, one-time)
  • How routing works with convention-based route configuration
  • How to compose dynamic components and validate forms

Why Aurelia Matters

Aurelia’s philosophy is “conventions over configuration.” Think of it like a well-organized office: instead of telling someone where every file goes (configuration), you follow a consistent system where pens go in the pen drawer and files go in the filing cabinet (conventions). This means you write less code to get started. The same approach powers DodaTech’s internal tools — when applications share a consistent structure, developers can jump between projects without re-learning everything. For security tools like Durga Antivirus Pro, Aurelia’s modular architecture makes it easy to swap out components without breaking the whole system.

    flowchart LR
    A[JavaScript & HTML Basics] --> B[Aurelia Fundamentals]
    B --> C[Components & Templates]
    B --> D[Dependency Injection]
    C --> E[Data Binding System]
    D --> E
    E --> F[Routing & Navigation]
    F --> G[Full Aurelia Application]
    style B fill:#4a90d9,color:#fff
  
Prerequisites: You should be comfortable with JavaScript (ES6+ classes, modules, decorators) and HTML. Familiarity with MVC patterns or other frameworks like Angular will help but is not required.

Core Concepts — How Aurelia Thinks

Aurelia flips the usual framework approach on its head. Instead of writing configuration files to tell the framework how to wire things together, Aurelia follows conventions — naming and file patterns that it recognizes automatically.

A Basic Component

Every Aurelia component is a pair: a JavaScript class (the view-model) and an HTML template (the view).

// my-app.js — the view-model (logic layer)
export class MyApp {
  constructor() {
    // This property is automatically available in the HTML template
    this.message = "Hello Aurelia!";
  }
}
<!-- my-app.html — the view (presentation layer) -->
<template>
  <!-- ${message} displays the property from the view-model -->
  <h1>${message}</h1>

  <!-- value.bind creates two-way binding: typing updates the property -->
  <input value.bind="message">
</template>

Why two files? Separating logic from presentation is a core software principle. The view-model handles data and behavior; the template handles display. If you later change how the input looks, you touch only the HTML. If you change how data loads, you touch only the JavaScript.

How Conventions Work

Aurelia follows a naming rule: a file named my-app.js automatically pairs with my-app.html. When you use <my-app> in another template, Aurelia finds the matching class automatically. No imports, no registration — it just works.

Dependency Injection — The Smart Parts Bin

Dependency Injection (DI) solves a common problem: when one class needs another class to work, how do they get connected? Without DI, you create dependencies manually inside each class, making them hard to test and change.

import { inject } from "aurelia-framework";
import { HttpClient } from "aurelia-fetch-client";

// @inject tells Aurelia: "this class needs an HttpClient instance"
@inject(HttpClient)
export class UserService {
  constructor(httpClient) {
    // Aurelia creates the HttpClient and passes it here automatically
    this.http = httpClient;
  }

  async getUsers() {
    const response = await this.http.fetch("/api/users");
    return response.json();
  }
}

Line by line: The @inject decorator declares that UserService requires HttpClient. When Aurelia creates a UserService, it looks at the constructor’s parameters, creates any that are missing, and passes them in. This means you can swap HttpClient with a mock version during testing — no changes to UserService needed.

Data Binding — Keeping Data and UI in Sync

Aurelia offers three binding modes. Choosing the right one affects performance and behavior:

<template>
  <!-- One-way binding: JS → HTML only -->
  <!-- The display updates when message changes, but typing in the input
       does NOT change message -->
  <p>${message}</p>
  <input value.one-way="message">

  <!-- Two-way binding: JS ↔ HTML — changes in either place sync -->
  <input value.two-way="message">

  <!-- One-time binding: JS → HTML once, then no updates -->
  <!-- Best for static values that never change after load -->
  <p>${message}</p>
  <input value.one-time="message">

  <!-- Shorthand: .bind detects the best mode automatically -->
  <!-- For form elements, it defaults to two-way -->
  <input value.bind="message">
</template>

When to use each: Use one-way for display-only data (a user’s name shown in a profile). Use two-way for forms where the user edits data. Use one-time for static content that never changes after page load — this is the most performant option because Aurelia doesn’t watch for changes.

Routing — Navigation Made Simple

Aurelia’s router maps URLs to components using conventions:

import { Router } from "aurelia-router";

export class App {
  configureRouter(config, router) {
    config.map([
      // Visiting /users loads the users.js/users.html component pair
      { route: "users", moduleId: "users", title: "Users" },
      // :id is a route parameter — /users/42 passes 42 as the id
      { route: "users/:id", moduleId: "user-detail", title: "User Detail" },
      // Empty route defaults to home
      { route: "", redirect: "users" }
    ]);
  }
}

Why this matters: Each route automatically loads the corresponding component. The router also handles navigation history, URL generation, and lifecycle hooks — so you can pause navigation if the user has unsaved changes.

Composition — Dynamic Components

The <compose> element lets you inject components dynamically at runtime:

<template>
  <!-- Dynamic composition: view-model is determined at runtime -->
  <compose view-model="${currentView}"></compose>
</template>
export class DynamicPage {
  constructor() {
    // This could come from user input, permissions, or configuration
    this.currentView = "admin-panel";
  }
}

Why composition: Instead of using if/else blocks to show different components, you let the data decide. This is how content management systems render different page types from a single layout.


Common Mistakes

  1. Forgetting to pair .js and .html files: Aurelia conventions require both files to exist with matching names. Having only one file without the other causes a silent failure — the component simply doesn’t render.

  2. Using .bind when you mean .one-way or .two-way: The shorthand .bind chooses the mode automatically, but sometimes you want to be explicit. Using .one-time on read-only data improves performance noticeably.

  3. Not using @inject for dependencies: If you manually new up dependencies in your constructor, you lose the ability to swap them during testing. Always use DI for services, HTTP clients, and other shared resources.

  4. Ignoring the activate lifecycle hook for async data: Loading data in the constructor is a common mistake. Use the activate() lifecycle method instead — it’s designed for async operations and works with the router’s navigation lifecycle.

  5. Over-nesting compose components: Deep compose chains are hard to debug and slow to render. Compose one level deep at most, and use routes for multi-level navigation.

Practice Questions

  1. What is the difference between .one-way and .two-way binding? One-way binding only updates the view when the model changes. Two-way binding also updates the model when the user interacts with the view — essential for form inputs.

  2. Why would you use dependency injection instead of creating dependencies in the constructor? DI makes testing easier — you can inject mock versions of dependencies. It also promotes loose coupling between classes.

  3. What does the @inject decorator do? It tells Aurelia’s DI container which dependencies a class requires. Aurelia automatically creates and passes instances of those dependencies when the class is instantiated.

  4. How does Aurelia know which template to pair with a JavaScript class? Aurelia follows naming conventions: my-component.js pairs with my-component.html. Both files must exist in the same directory.

  5. When should you use the activate lifecycle hook? For loading data when a route is navigated to. It’s async-friendly and integrated with the router’s loading indicators.

Challenge

Build a contact manager app: Create an Aurelia app with a route for listing contacts (fetched from a mock API service via DI), a route for viewing a single contact (contacts/:id), and a search input with two-way binding that filters the contact list in real time. Use @inject for the data service.

FAQ

What is the difference between Aurelia 1 and Aurelia 2?
Aurelia 2 is a complete rewrite with better performance, improved TypeScript support, a more modular architecture, and enhanced composability. It drops some legacy conventions from Aurelia 1.
Is Aurelia production-ready?
Yes. Aurelia has been used in production by enterprise organizations for years. Version 2 is stable and actively maintained.
Does Aurelia support TypeScript?
Yes. Aurelia has first-class TypeScript support with full type definitions and a TypeScript-based application skeleton.
How does Aurelia compare to Angular?
Both use dependency injection and conventions, but Aurelia is lighter, has a gentler learning curve, and requires less boilerplate. Angular has a larger ecosystem and more built-in features.

Try It Yourself

Create a simple Aurelia component using a CDN-based HTML file:

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/aurelia-script@1.5.0/dist/aurelia-esnext.js"></script>
</head>
<body aurelia-app>
  <my-app></my-app>

  <script>
    // The view-model
    class MyApp {
      constructor() {
        this.name = "Aurelia Developer";
        this.items = ["Learn binding", "Build an app", "Deploy"];
      }

      addItem() {
        if (this.newItem) {
          this.items.push(this.newItem);
          this.newItem = "";
        }
      }
    }
  </script>

  <!-- The template — Aurelia pairs these by convention -->
  <template>
    <h2>Hello, ${name}!</h2>
    <input value.bind="newItem" placeholder="Add item..." />
    <button click.trigger="addItem()">Add</button>
    <ul>
      <li repeat.for="item of items">${item}</li>
    </ul>
  </template>
</body>
</html>

Expected output: A page showing “Hello, Aurelia Developer!” with an input, an Add button, and a list of items that grows when you click Add.

What’s Next

TopicDescription
Backbone.js Guide
Compare Aurelia with a lightweight alternative
HTML Fundamentals
Review the template language Aurelia uses
TypeScript Basics
Add types to your Aurelia project

Related terms: JavaScript, HTML, TypeScript, Angular, Node.js

What’s Next

Congratulations on completing this Aurelia tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro