Skip to content

Core Concepts

EreoJS is built on a few fundamental concepts that work together to create a cohesive full-stack development experience. Understanding these concepts will help you build applications effectively.

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                      Browser                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │   Islands   │  │ Navigation  │  │  Form Actions   │  │
│  │  (Hydrated) │  │  (SPA-like) │  │  (Progressive)  │  │
│  └─────────────┘  └─────────────┘  └─────────────────┘  │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│                     EreoJS Server                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │   Router    │  │   Loaders   │  │    Actions      │  │
│  │  (File-based)│ │ (Data fetch)│  │  (Mutations)    │  │
│  └─────────────┘  └─────────────┘  └─────────────────┘  │
│                                                         │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │  Middleware │  │   Cache     │  │   Streaming     │  │
│  └─────────────┘  └─────────────┘  └─────────────────┘  │
└─────────────────────────────────────────────────────────┘

Core Principles

Web Standards First

EreoJS uses standard web APIs throughout:

  • Request/Response - All data flows through standard HTTP primitives
  • FormData - Forms use the platform's native form handling
  • URLSearchParams - Query strings use standard parsing
  • Headers - HTTP headers are manipulated with the Headers API

This means your knowledge transfers directly to other tools and platforms.

Progressive Enhancement

Applications work without JavaScript and enhance when it's available:

  1. HTML first - Server renders complete HTML
  2. Forms work - Standard forms submit without JS
  3. JS enhances - Client-side navigation, optimistic updates
  4. Islands hydrate - Only interactive components get JavaScript

Explicit Over Magic

EreoJS prefers explicit configuration over conventions that hide behavior:

  • Caching is opt-in - You decide what gets cached
  • Hydration is explicit - You mark what needs JavaScript
  • Data loading is visible - Loaders are functions you write

The Request Lifecycle

Request → Router → Middleware → Loader → Render → Response

           Cache (if configured)
  1. Request arrives at the Bun server
  2. Router matches the URL to a route file
  3. Middleware runs (auth, logging, etc.)
  4. Loader executes to fetch data (may use cache)
  5. Component renders with loader data
  6. Response streams HTML to the browser

For mutations:

Form Submit → Router → Middleware → Action → Revalidate → Response

Key Concepts

Routing

File-based routing that maps your directory structure to URLs:

routes/
├── index.tsx        → /
├── about.tsx        → /about
├── posts/
│   ├── index.tsx    → /posts
│   └── [id].tsx     → /posts/:id

Data Loading

Loaders fetch data before rendering:

tsx
export const loader = createLoader(async ({ params }) => {
  const post = await db.posts.find(params.id)
  return { post }
})

Actions handle form submissions:

tsx
export const action = createAction(async ({ request }) => {
  const data = await request.formData()
  await db.posts.create(data)
  return redirect('/posts')
})

Rendering Modes

Choose how each route renders:

  • SSR - Server-side rendering (default)
  • SSG - Static site generation at build time
  • CSR - Client-side rendering
  • Streaming - Progressive HTML streaming with Suspense

Islands Architecture

Selective hydration for interactive components. Add 'use client' to mark a component for client-side hydration:

tsx
// app/components/Counter.tsx
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Only island components ship JavaScript — the rest of the page is static HTML.

Caching

Tag-based cache invalidation:

tsx
export const config = {
  cache: {
    maxAge: 3600,
    tags: ['posts', `post-${id}`]
  }
}

// Invalidate when needed
await revalidateTag('posts')

Middleware

Request/response processing pipeline:

tsx
export const middleware = [
  'auth',        // Named middleware
  'rateLimit',
  async (request, context, next) => {  // Inline function
    console.log(request.url)
    return next()
  }
]

Data Flow

┌──────────────────────────────────────────────────────────┐
│                        Server                            │
│                                                          │
│  loader() ──────────────────────────► { data }          │
│      │                                    │              │
│      │                                    ▼              │
│      │                             Component(data)       │
│      │                                    │              │
│      ▼                                    ▼              │
│   Cache                               HTML Stream        │
│                                                          │
└──────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────┐
│                       Browser                            │
│                                                          │
│  HTML ──► Islands Hydrate ──► Interactive Components    │
│                                                          │
│  Form Submit ──► action() ──► Revalidate ──► Re-render  │
│                                                          │
└──────────────────────────────────────────────────────────┘

Component Model

Route Components

Export a default function component:

tsx
// routes/posts/[id].tsx
export default function Post({ loaderData, params }) {
  return <h1>{loaderData.post.title}</h1>
}

Props available:

  • loaderData - Data from the loader
  • actionData - Result from the action
  • params - URL parameters
  • searchParams - Query string parameters

Layout Components

Wrap child routes:

tsx
// routes/_layout.tsx
export default function Layout({ children }) {
  return (
    <div>
      <nav>...</nav>
      <main>{children}</main>
    </div>
  )
}

Island Components

Interactive components that hydrate on the client. Mark them with 'use client':

tsx
// app/components/Counter.tsx
'use client';

import { useState } from 'react';

export function Counter({ initialCount = 0 }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

Use them directly in routes:

tsx
import { Counter } from '~/components/Counter';

export default function Home() {
  return (
    <div>
      <h1>Static HTML</h1>
      <Counter initialCount={0} />  {/* Only this hydrates */}
    </div>
  );
}

Type Safety

EreoJS provides full TypeScript support:

tsx
import type { LoaderFunction, ActionFunction } from '@ereo/core'

interface Post {
  id: string
  title: string
}

export const loader: LoaderFunction<{ post: Post }> = async ({ params }) => {
  const post = await getPost(params.id)
  return { post }
}

export default function Post({ loaderData }: { loaderData: { post: Post } }) {
  return <h1>{loaderData.post.title}</h1>
}

Next Steps

Dive deeper into each concept:

Released under the MIT License.