logo

Runtime

Lightweight frontend runtime for SPA applications without frameworks

Built for developers who want control without framework overhead

The runtime provides a lightweight foundation for building modern single-page applications without relying on large frontend frameworks. It is designed around a modular architecture that separates concerns such as dependency injection, routing, reactivity, and rendering into independent but cohesive parts.

Developers can build fully functional applications using a minimal set of primitives while maintaining full control over application structure and behavior. The runtime includes a strict dependency injection system, a client-side router with navigation guards, and a reactive state model powered by Proxy. Components follow a predictable lifecycle, ensuring consistent initialization, rendering, and cleanup across the application.

Template bindings allow declarative DOM updates using simple data attributes, reducing the need for manual DOM manipulation. The runtime is framework-independent and has zero external dependencies, making it suitable for long-term maintenance and custom architectures. It integrates seamlessly with the build system and templates, enabling a smooth development experience from project creation to deployment. This approach allows you to build scalable applications with clear architecture, without sacrificing performance or flexibility.

Quick Start

Bootstrap
import { bootstrapApplication, provideRouter } from '@razerspine/runtime';

bootstrapApplication({
  providers: [
    provideRouter([
      { path: '/', component: HomePage }
    ])
  ]
});

Core Features

DI Container

Strict dependency injection system

Router

SPA navigation with guards

Reactivity

Proxy-based reactive state

Components

Lifecycle-driven architecture

Component Example

Component
export class HomePage extends BaseComponent {
  increment() {
    this.setState({ count: this.state.count + 1 });
  }
}

Architecture

Modules
core → router → reactivity → view → http → platform

Common Mistakes & Anti-patterns

Common pitfalls when working with the runtime and how to avoid them

DI Container
❌ Injecting service without registering it
✔ Always provide services in bootstrapApplication()

❌ Expecting automatic instantiation
✔ Runtime uses strict DI mode (no auto new)

❌ Circular dependencies between services
✔ Keep services isolated and avoid cross-dependency loops

The DI container works in strict mode. Services are not auto-created and must be explicitly registered during bootstrap. If a service is missing, the runtime will throw an error instead of silently failing, making configuration issues easier to detect.

State Management
❌ Direct state mutation outside of setState
✔ Always use setState() for predictable updates

❌ Replacing state object entirely
✔ State is Proxy-based and must remain stable

❌ Mutating deeply without understanding reactivity
✔ Use structured updates to keep UI in sync

The reactive system is based on Proxy. Direct mutation may work, but breaks predictable update flow. setState() ensures controlled updates and keeps rendering consistent.

Component Lifecycle
❌ Performing async logic inside render()
✔ Use onInit() for async operations

❌ Forgetting cleanup for manual subscriptions
✔ Use onDestroy() for cleanup

❌ Manipulating DOM outside render()
✔ Let bindings handle DOM updates

The lifecycle is strictly ordered: render → bind events → update → onInit. Async logic should be placed in onInit() to avoid inconsistent rendering behavior.

Router
❌ Calling Router before bootstrap completes
✔ Router is initialized inside bootstrapApplication

❌ Ignoring guard return types
✔ Guards must return true | false | redirect string

❌ Assuming navigation errors will crash app
✔ Router handles errors internally

The Router is designed to be fault-tolerant. Navigation errors are caught internally and do not break the application lifecycle. Optional error handlers can be attached for custom UI behavior.

MPA Mode
❌ Forgetting to call applyBindings()
✔ Must be called manually after state updates

❌ Using bindForms without applyBindings
✔ They work together as a pair

❌ Not cleaning up listeners
✔ Always remove listeners to avoid memory leaks

In MPA mode, lifecycle is fully manual. You must initialize bindings, attach event handlers and handle cleanup yourself. Forgetting cleanup may lead to memory leaks or duplicated listeners.

Bindings Engine
❌ Relying on incorrect processing order
✔ data-for always runs before other bindings

❌ Mixing manual DOM updates with bindings
✔ Let applyBindings control DOM synchronization

❌ Misusing data-model on unsupported elements
✔ Use proper input types for two-way binding

The bindings engine processes directives in a strict order. Structural changes like data-for must be applied first to ensure correct DOM updates. Mixing manual DOM manipulation with bindings may lead to inconsistent UI state.

Advanced Patterns & Best Practices

This section describes recommended architectural patterns and best practices for building scalable applications using the runtime. These patterns are based on real-world usage and internal runtime behavior.

Following these guidelines helps maintain predictable data flow, improves performance and prevents common architectural mistakes when working with reactive state, dependency injection and routing.

Keep Components Stateless Where Possible

Move business logic into services instead of components. Components should focus on rendering and user interaction, while services handle data and side effects.

Use DI for Shared Logic

Always register shared services in the DI container and access them via inject(). This ensures a single source of truth and avoids duplicated logic across components.

Avoid Direct State Mutation

Always use setState() instead of mutating state directly. This guarantees that the reactive system triggers DOM updates correctly.

Keep State Minimal

Store only the data required for rendering. Derived values should be computed inside methods instead of being stored in the reactive state.

Handle Async in onInit

Perform API calls and async operations inside onInit(). The lifecycle supports async execution and ensures proper rendering flow.

Clean Up Side Effects

Always clean up intervals, subscriptions or manual listeners inside onDestroy() to prevent memory leaks, especially in SPA navigation.

Use Router for Navigation

Avoid manipulating window.location manually. Use Router.navigate() to keep navigation consistent and lifecycle-safe.

Split Large Features

Break large pages into smaller logical units and services. This improves maintainability and aligns with the modular runtime architecture.