Ad – 728×90
🦉 Getting Started

OWL JS Project Structure – How to Organise Your Code

As your OWL JS application grows from a single file into dozens of components, having a clear file structure becomes essential. This lesson covers two project structures: a standalone OWL JS app (when you are building outside of Odoo) and an Odoo module with OWL JS front-end code (the real-world structure you will use in production Odoo work). You will also learn naming conventions, what each file type does, and how Odoo's asset bundling system loads your OWL components.

⏱️ 16 min read 🎯 Beginner 📅 Updated 2026

Standalone OWL JS Project Structure

For a standalone OWL JS app built with Vite + npm (Approach 3 from the Setup lesson), the recommended structure is:

Directory structure – Standalone OWL + Vite
my-owl-app/
├── index.html                  ← HTML entry point, contains <div id="app">
├── package.json                ← npm config: @odoo/owl dependency, scripts
├── vite.config.js              ← Vite build config
│
├── src/
│   ├── main.js                 ← App entry: imports App, calls mount()
│   │
│   ├── App.js                  ← Root component — composes everything else
│   │
│   ├── components/             ← Reusable UI components
│   │   ├── Button.js
│   │   ├── Modal.js
│   │   └── DataTable.js
│   │
│   ├── views/                  ← Page-level components (routed views)
│   │   ├── HomeView.js
│   │   ├── UserListView.js
│   │   └── SettingsView.js
│   │
│   ├── hooks/                  ← Custom OWL hooks (reusable logic)
│   │   ├── useLocalStorage.js
│   │   └── useFetch.js
│   │
│   ├── services/               ← App-wide services (injectable via env)
│   │   ├── ApiService.js
│   │   └── NotificationService.js
│   │
│   └── utils/                  ← Helper functions (not components)
│       ├── formatDate.js
│       └── validators.js
│
└── tests/                      ← Unit tests for components and hooks
    ├── App.test.js
    └── components/
        └── Button.test.js

Key files explained

FilePurpose
index.htmlThe HTML shell. Only contains <div id="app"> and a <script type="module"> pointing to src/main.js
src/main.jsImports the root App component and calls mount(App, document.getElementById("app")). May also configure the OWL app environment here.
src/App.jsThe root component. Defines the top-level layout and registers all top-level sub-components.
src/components/Reusable, stateless-or-self-contained UI pieces: buttons, inputs, modals, cards. Components here should work in any context.
src/views/Page-level components associated with routes or major app screens. They compose components/ pieces together.
src/hooks/Custom OWL hooks — functions that call useState, onMounted, etc. to encapsulate reusable stateful logic.
src/services/Services shared across the app via OWL's environment system. Examples: API client, notification manager, auth service.
Ad – 336×280

Odoo Module Structure with OWL JS

When building a real Odoo module, your OWL JS code lives inside a Python module directory. Odoo has a specific convention for where front-end assets (JavaScript, CSS, templates) go. Here is the anatomy of a custom Odoo module named my_custom_module:

Directory structure – Odoo Module with OWL JS
my_custom_module/
├── __init__.py                 ← Python: makes this a Python package
├── __manifest__.py             ← Odoo: module name, version, depends, data files
│
├── models/                     ← Python: Odoo ORM models (database)
│   ├── __init__.py
│   └── sale_order_custom.py
│
├── views/                      ← XML: Odoo form/tree/kanban view definitions
│   └── sale_order_views.xml
│
├── security/                   ← CSV/XML: access rights and record rules
│   └── ir.model.access.csv
│
└── static/                     ← ALL front-end assets (served to browser)
    ├── description/
    │   └── icon.png            ← Module icon shown in Odoo App Store
    │
    └── src/                    ← OWL JS source code
        ├── js/
        │   ├── components/     ← OWL components
        │   │   ├── MyWidget.js
        │   │   └── CustomKanban.js
        │   ├── views/          ← OWL view overrides (extends Odoo views)
        │   │   └── sale_form_view.js
        │   └── main.js         ← Entry point: registers components
        │
        ├── xml/                ← OWL templates (alternative to xml`` helper)
        │   └── my_templates.xml
        │
        └── css/                ← Custom CSS for your components
            └── my_module.css
ℹ️
The static/ directory is special in Odoo

Odoo serves files inside static/ directly to the browser without any Python processing. This is where all your JavaScript, CSS, and image assets live. Odoo's asset bundling system (defined in __manifest__.py or ir.asset records) tells Odoo which JS and CSS files to include in the browser bundle.

The __manifest__.py File

The manifest tells Odoo about your module and declares your front-end assets:

Python – __manifest__.py
{
    "name": "My Custom Module",
    "version": "17.0.1.0.0",
    "category": "Sales",
    "summary": "Custom OWL widgets for Sales",
    "author": "Your Company",
    "depends": ["sale", "web"],      # Odoo modules this depends on
    "license": "LGPL-3",

    # Declare your JavaScript and CSS assets
    "assets": {
        # web.assets_backend = the main Odoo backend bundle
        "web.assets_backend": [
            "my_custom_module/static/src/js/components/MyWidget.js",
            "my_custom_module/static/src/js/views/sale_form_view.js",
            "my_custom_module/static/src/css/my_module.css",
        ],
    },

    # XML views, security files, and data
    "data": [
        "security/ir.model.access.csv",
        "views/sale_order_views.xml",
    ],

    "installable": True,
    "application": False,
}

Registering OWL Components in Odoo

In a standalone app you call mount(App, el) to start OWL. In Odoo, you do not call mount() directly — instead you extend or patch existing Odoo views and components using Odoo's registry system:

JavaScript – Registering a custom field widget in Odoo
/** @odoo-module */
// The @odoo-module comment tells Odoo's bundler this is an ES module

import { Component, xml, useState } from "@web/owl";
// In Odoo, OWL is imported from "@web/owl" (Odoo's internal alias)

// Import Odoo's registry to register your component as a field widget
import { registry } from "@web/core/registry";

// Your custom OWL component
class ColorPickerWidget extends Component {
  static template = xml`
    <div class="color-picker-widget">
      <div t-foreach="colors" t-as="color" t-key="color"
           t-att-style="'background:' + color"
           t-att-class="{ selected: props.value === color }"
           t-on-click="() => props.update(color)">
      </div>
    </div>
  `;

  get colors() {
    return ["#e74c3c", "#3498db", "#2ecc71", "#f39c12", "#9b59b6"];
  }
}

// Register as a field widget — Odoo will use this for fields
// where widget="color_picker" is specified in a view XML
registry.category("fields").add("color_picker", ColorPickerWidget);
⚠️
In Odoo: import from "@web/owl", not "@odoo/owl"

Inside an Odoo module, OWL is available as @web/owl — Odoo provides it through its own asset system. Do not try to install @odoo/owl via npm inside an Odoo module. Odoo also provides its own useState, Component, etc. through this alias. When learning OWL standalone (this course), use @odoo/owl or the CDN. When writing real Odoo modules, use @web/owl.

Naming Conventions

ItemConventionExample
Component filePascalCase, matches class nameUserCard.js, SalesKanban.js
Component classPascalCaseclass UserCard extends Component
Hook filecamelCase, prefixed useuseSearch.js, useNotification.js
Service filecamelCase + "Service"notificationService.js
CSS filesnake_case (Odoo convention)my_module.css
Template IDsModuleName.ComponentNameMyModule.UserCard
Odoo module dirsnake_casemy_custom_module/

Anatomy of a Single Component File

Here is what a well-structured OWL JS component file looks like:

JavaScript – UserCard.js (anatomy)
// 1. OWL imports
import { Component, xml, useState, onMounted } from "@odoo/owl";

// 2. Sub-component imports (if any)
import { Avatar } from "./Avatar.js";
import { Badge } from "./Badge.js";

// 3. Component class
export class UserCard extends Component {

  // 4. Static props declaration — documents and validates incoming props
  static props = {
    userId: Number,
    name: String,
    role: { type: String, optional: true },
    onDelete: { type: Function, optional: true },
  };

  // 5. Default props (optional)
  static defaultProps = {
    role: "Member",
  };

  // 6. Sub-component registry
  static components = { Avatar, Badge };

  // 7. Reactive state
  state = useState({ expanded: false });

  // 8. Lifecycle hooks — called in setup()
  setup() {
    onMounted(() => {
      console.log(`UserCard for user ${this.props.userId} mounted`);
    });
  }

  // 9. Computed getters
  get initials() {
    return this.props.name
      .split(" ")
      .map(n => n[0])
      .join("")
      .toUpperCase();
  }

  // 10. Event handlers
  toggleExpanded() {
    this.state.expanded = !this.state.expanded;
  }

  handleDelete() {
    if (this.props.onDelete) {
      this.props.onDelete(this.props.userId);
    }
  }

  // 11. Template — defined last so methods are visible above it
  static template = xml`
    <div class="user-card" t-att-class="{ expanded: state.expanded }">
      <Avatar initials="initials"/>
      <div class="user-info">
        <h3 t-esc="props.name"/>
        <Badge label="props.role"/>
      </div>
      <button t-on-click="toggleExpanded">
        <t t-if="state.expanded">▲ Less</t>
        <t t-else="">▼ More</t>
      </button>
      <div t-if="state.expanded" class="user-details">
        <p>ID: <t t-esc="props.userId"/></p>
        <button t-if="props.onDelete" t-on-click="handleDelete">Delete</button>
      </div>
    </div>
  `;
}

📋 Summary

  • Standalone OWL projects: organise into components/, views/, hooks/, services/, utils/.
  • Odoo modules: all front-end assets live in static/src/. Assets are declared in __manifest__.py under the assets key.
  • In Odoo, OWL components are registered in Odoo's registry system (not mounted directly).
  • In Odoo modules, import OWL from "@web/owl" — NOT "@odoo/owl".
  • Naming: PascalCase for component files and classes; camelCase for hooks and utilities; snake_case for Odoo module directories.
  • A well-structured component file: imports → class → static props → static components → state → setup() → getters → event handlers → template.

Frequently Asked Questions

Can I put multiple OWL components in a single file? +

Yes — you can export multiple classes from one file. For small, tightly related components (like a parent and a private child it exclusively uses), a single file is fine. For components that are reused in many places, give each one its own file. The general rule: one public component per file, with small private helpers in the same file if they are not reused elsewhere.

What is the @odoo-module comment in Odoo module files? +

The /** @odoo-module */ comment at the top of a JavaScript file tells Odoo's asset bundler to treat the file as an ES module with import/export syntax. Without it, Odoo assumes the file is a legacy script. In modern Odoo 16/17 development you should always include it at the top of every JavaScript file in your module.

Where do OWL templates defined in separate XML files go? +

In Odoo, you can define OWL templates in separate .xml files inside static/src/xml/. These XML files use the same t-* directive syntax as inline xml`` templates. You reference them by template ID (e.g. MyModule.MyComponent). Separate XML templates are useful for very long templates that would clutter your JS file. In standalone OWL apps, the inline xml`` helper is simpler and is the modern preferred approach.