Skip to content

Project Structure

Cloudwerk uses a file-based routing system where your directory structure defines your application’s routes. This guide covers all the file conventions you need to know.

A typical Cloudwerk project has this structure:

  • Directoryapp/
    • page.tsx # Home page (/)
    • layout.tsx # Root layout
    • middleware.ts # Global middleware
    • error.tsx # Error boundary
    • not-found.tsx # 404 page
    • Directoryabout/
      • page.tsx # About page (/about)
    • Directoryusers/
      • page.tsx # Users list (/users)
      • Directory[id]/
        • page.tsx # User profile (/users/:id)
        • Directoryedit/
          • page.tsx # Edit user (/users/:id/edit)
    • Directoryapi/
      • Directoryhealth/
        • route.ts # Health endpoint (/api/health)
      • Directoryusers/
        • route.ts # Users API (/api/users)
        • Directory[id]/
          • route.ts # User API (/api/users/:id)
  • Directorypublic/ # Static assets
  • cloudwerk.config.ts # Configuration
  • wrangler.toml # Cloudflare config
  • package.json
  • tsconfig.json

The page.tsx file defines a publicly accessible page at that route:

// app/about/page.tsx -> /about
export default function AboutPage() {
return <h1>About Us</h1>;
}

Pages can export a loader() function for server-side data fetching:

export async function loader({ params, context }: LoaderArgs) {
return { data: await fetchData() };
}
export default function Page({ data }: PageProps & { data: Data }) {
return <div>{/* render data */}</div>;
}

The layout.tsx file wraps pages and nested layouts:

// app/layout.tsx
export default function RootLayout({ children }: LayoutProps) {
return (
<html>
<body>{children}</body>
</html>
);
}

Layouts can also have their own loader() functions:

export async function loader({ context }: LoaderArgs) {
const user = await context.auth.getUser();
return { user };
}
export default function DashboardLayout({ children, user }: LayoutProps & { user: User }) {
return (
<div>
<Sidebar user={user} />
<main>{children}</main>
</div>
);
}

The route.ts file defines API endpoints that handle HTTP methods:

// app/api/users/route.ts
import { json } from '@cloudwerk/core';
export async function GET(request: Request, { context }: CloudwerkHandlerContext) {
const users = await context.db.selectFrom('users').execute();
return json(users);
}
export async function POST(request: Request, { context }: CloudwerkHandlerContext) {
const body = await request.json();
const user = await context.db.insertInto('users').values(body).execute();
return json(user, { status: 201 });
}

Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS

The middleware.ts file runs before route handlers:

// app/middleware.ts
import type { Middleware } from '@cloudwerk/core';
export const middleware: Middleware = async (request, next) => {
// Run before the route handler
console.log(`${request.method} ${request.url}`);
const response = await next(request);
// Run after the route handler
return response;
};

Middleware is inherited by child routes. Place middleware at any level:

  • Directoryapp/
    • middleware.ts # Runs for all routes
    • Directoryadmin/
      • middleware.ts # Runs for /admin/* routes
      • page.tsx

The error.tsx file handles errors in that route segment:

// app/error.tsx
'use client';
export default function ErrorPage({ error, reset }: ErrorProps) {
return (
<div>
<h1>Something went wrong</h1>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}

The not-found.tsx file handles 404 errors:

// app/not-found.tsx
export default function NotFoundPage() {
return (
<div>
<h1>404 - Page Not Found</h1>
<a href="/">Go home</a>
</div>
);
}

Use brackets for dynamic segments:

app/users/[id]/page.tsx -> /users/:id

Access params in your loader or component:

export async function loader({ params }: LoaderArgs) {
// params.id is the dynamic value
return { user: await getUser(params.id) };
}

Use [...slug] to catch all subsequent segments:

app/docs/[...slug]/page.tsx -> /docs/*
export async function loader({ params }: LoaderArgs) {
// params.slug is an array like ['guides', 'routing']
const path = params.slug.join('/');
return { doc: await getDoc(path) };
}

Use [[...slug]] for optional catch-all:

app/shop/[[...categories]]/page.tsx
-> /shop
-> /shop/electronics
-> /shop/electronics/phones

Use (groupName) to organize routes without affecting the URL:

  • Directoryapp/
    • Directory(marketing)/
      • page.tsx # /
      • Directoryabout/
        • page.tsx # /about
    • Directory(dashboard)/
      • layout.tsx # Dashboard layout
      • Directorydashboard/
        • page.tsx # /dashboard
      • Directorysettings/
        • page.tsx # /settings

You can colocate components, utilities, and tests alongside your routes:

  • Directoryapp/
    • Directoryusers/
      • page.tsx
      • UserCard.tsx # Component (not a route)
      • user.utils.ts # Utility (not a route)
      • page.test.ts # Test (not a route)

Only page.tsx, layout.tsx, route.ts, and middleware.ts are treated as routes.