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.
Overview
Section titled “Overview”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
File Conventions
Section titled “File Conventions”page.tsx
Section titled “page.tsx”The page.tsx file defines a publicly accessible page at that route:
// app/about/page.tsx -> /aboutexport 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>;}layout.tsx
Section titled “layout.tsx”The layout.tsx file wraps pages and nested layouts:
// app/layout.tsxexport 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) { // Access session from cookies or middleware-set values const session = context.req.cookie('session'); const user = session ? context.get('user') : null; return { user };}
export default function DashboardLayout({ children, user }: LayoutProps & { user: User }) { return ( <div> <Sidebar user={user} /> <main>{children}</main> </div> );}route.ts
Section titled “route.ts”The route.ts file defines API endpoints that handle HTTP methods:
// app/api/users/route.tsimport type { CloudwerkHandlerContext } from '@cloudwerk/core';import { json, getContext } from '@cloudwerk/core';
export async function GET(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext(); const { results: users } = await env.DB.prepare('SELECT * FROM users').all(); return json(users);}
export async function POST(request: Request, { params }: CloudwerkHandlerContext) { const { env } = getContext(); const body = await request.json(); const id = crypto.randomUUID(); await env.DB .prepare('INSERT INTO users (id, name, email) VALUES (?, ?, ?)') .bind(id, body.name, body.email) .run(); return json({ id, ...body }, { status: 201 });}Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
middleware.ts
Section titled “middleware.ts”The middleware.ts file runs before route handlers:
// app/middleware.tsimport 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();
// 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
error.tsx
Section titled “error.tsx”The error.tsx file handles errors in that route segment:
// app/error.tsximport type { ErrorBoundaryProps } from '@cloudwerk/core';
export default function ErrorPage({ error, errorType }: ErrorBoundaryProps) { return ( <div> <h1>Something went wrong</h1> <p>{error.message}</p> {error.digest && <p>Error ID: {error.digest}</p>} <a href="/">Go home</a> </div> );}not-found.tsx
Section titled “not-found.tsx”The not-found.tsx file handles 404 errors:
// app/not-found.tsxexport default function NotFoundPage() { return ( <div> <h1>404 - Page Not Found</h1> <a href="/">Go home</a> </div> );}Dynamic Routes
Section titled “Dynamic Routes”Single Segment
Section titled “Single Segment”Use brackets for dynamic segments:
app/users/[id]/page.tsx -> /users/:idAccess params in your loader or component:
export async function loader({ params }: LoaderArgs) { // params.id is the dynamic value return { user: await getUser(params.id) };}Catch-All Segments
Section titled “Catch-All Segments”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) };}Optional Catch-All
Section titled “Optional Catch-All”Use [[...slug]] for optional catch-all:
app/shop/[[...categories]]/page.tsx-> /shop-> /shop/electronics-> /shop/electronics/phonesRoute Groups
Section titled “Route Groups”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
Colocation
Section titled “Colocation”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.
Next Steps
Section titled “Next Steps”- Routing Guide - Deep dive into routing patterns
- Data Loading - Server-side data fetching
- Middleware - Request middleware patterns