<!-- Markdown mirror of https://justscale.sh/docs/fundamentals/routes -->

# Routes

Route builders with middleware, guards, and handlers

Routes define individual endpoints in your controllers. JustScale uses a fluent builder API that lets you chain middleware, guards, and handlers with full type safety at every step.

## Route Builders

Import route factories from the transport package: `Get`, `Post`, `Put`,`Delete`, `Patch` from `@justscale/http`, or `SSE` from `@justscale/sse`.

src/controllers/ticket-controller.tsTypeScript

```typescript
import { createController } from '@justscale/core';
import { Get, Post } from '@justscale/http';
import { SSE } from '@justscale/sse';
import { Ticket, HelpdeskService } from '../domain/models';

export const TicketController = createController('/tickets', {
  inject: { helpdesk: HelpdeskService },

  routes: ({ helpdesk }) => ({
    list:    Get('/'),
    get:     Get('/:ticket'),
    create:  Post('/'),
    resolve: Post('/:ticket/resolve'),
    close:   Post('/:ticket/close'),
    events:  SSE('/:ticket/events'),
  }),
});
```

## Path Parameters

Define path parameters with `:paramName` syntax. They're automatically extracted and typed in the handler context:

path-params.tsTypeScript

```typescript
import { Get } from '@justscale/http';

// Single param
Get('/:ticket').handle(async ({ params }) => {
  params.ticket; // string — inferred from the path
});

// Multiple params
Get('/:ticket/comments/:commentId').handle(async ({ params }) => {
  params.ticket;    // string
  params.commentId; // string
});
```

## Typed Parameters with .types()

Use `.types()` to transform path parameters from strings into model references. This is the JustScale way — no raw IDs in your handlers:

src/controllers/typed-params.tsTypeScript

```typescript
import { Get } from '@justscale/http';
import { Ticket } from '../domain/models';

// Without .types() — params.ticket is a string
Get('/:ticket').handle(async ({ params }) => {
  params.ticket; // string — a raw URL segment
});

// With .types() — params.ticket is Reference<Ticket>
Get('/:ticket')
  .types({ Ticket })
  .handle(async ({ params }) => {
    const ticket = await params.ticket; // Persistent<Ticket> | undefined
    ticket?.subject; // typed field access
  });
```

ℹ️Info

`.types({ Ticket })` matches `:ticket`, `:Ticket`, and `:ticketRef`. Each request gets a fresh Reference — no cross-request caching.

## The .handle() Method

Every route ends with `.handle()`. The handler receives a context object with transport-specific fields and anything added by middleware:

src/controllers/handlers.tsTypeScript

```typescript
import { Get, Post } from '@justscale/http';
import { z } from 'zod';
import { Ticket } from '../domain/models';

// GET handler — read a ticket
Get('/:ticket')
  .types({ Ticket })
  .handle(async ({ params, res }) => {
    const ticket = await params.ticket;
    if (!ticket) return res.status(404).json({ error: 'Not found' });
    res.json(ticket);
  });

// POST handler — create with validated body
const CreateTicketBody = z.object({
  subject: z.string().min(1),
  body: z.string(),
  priority: z.enum(['low', 'medium', 'high', 'critical']),
});

Post('/')
  .body(CreateTicketBody)
  .returns(201)
  .handle(async ({ body, res }) => {
    // body is typed: { subject: string, body: string, priority: ... }
    const ticket = await helpdesk.createTicket(
      body.subject, body.body, body.priority
    );
    res.status(201).json(ticket);
  });
```

## The .use() Method — Middleware

Chain middleware with `.use()`. Each middleware adds properties to the context, which accumulate through the chain:

middleware-chain.tsTypeScript

```typescript
import { Get } from '@justscale/http';
import { auth } from '@justscale/auth';

Get('/:ticket')
  .use(auth)              // adds { user: User } to context
  .use(loadCustomer)      // adds { customer: Customer }
  .handle(({ user, customer, params, res }) => {
    // Both user and customer are available and typed
    res.json({ user: user.name, customer: customer.name });
  });
```

## The .guard() Method

Guards gate access to a route. They run after middleware and can stop execution. Use model permissions for declarative authorization:

src/controllers/guarded-routes.tsTypeScript

```typescript
import { Post } from '@justscale/http';
import { auth } from '@justscale/auth';
import { Ticket } from '../domain/models';

// Permission guard — only agents can resolve
Post('/:ticket/resolve')
  .types({ Ticket })
  .use(auth)
  .guard(Ticket.can.resolve)
  .handle(async ({ params, res, user }) => {
    const ticket = await params.ticket;
    if (!ticket) return res.status(404).json({ error: 'Not found' });
    await helpdesk.resolveTicket(ticket, user.name);
    res.status(204).end();
  });
```

## Schema Validation

Use Zod schemas with `.body()` for automatic request validation and `.returns()` for response type declarations:

src/controllers/validated-route.tsTypeScript

```typescript
import { Post } from '@justscale/http';
import { auth } from '@justscale/auth';
import { CreateTicketBody, TicketSchema, ErrorSchema } from './schemas';

Post('/')
  .use(auth)
  .body(CreateTicketBody)
  .returns(201, TicketSchema)
  .returns(400, ErrorSchema)
  .handle(async ({ body, res, user }) => {
    // body is typed: { subject: string, body: string, priority: 'low' | ... }
    // Invalid bodies are rejected with 400 before the handler runs
    const ticket = await helpdesk.createTicket(
      body.subject,
      body.body,
      body.priority,
      Customer.ref(user),
    );
    res.status(201).json(ticket);
  });
```

## SSE Routes

SSE routes use the same builder API but the handler is an async generator:

src/controllers/sse-route.tsTypeScript

```typescript
import { SSE } from '@justscale/sse';
import { Ticket } from '../domain/models';

SSE('/:ticket/events')
  .types({ Ticket })
  .handle(async function* ({ params }) {
    const ticket = await params.ticket;
    if (!ticket) {
      yield { event: 'error', data: { message: 'Not found' } };
      return;
    }

    yield { event: 'connected', data: { subject: ticket.subject } };

    for await (const event of ticket.events) {
      yield { event: event.type, data: event.data };
    }
  });
```

## Route Execution Order

When a request comes in, steps execute in the order they were chained:

- 1. .use() steps — middleware adds to context, in order
- 2. .guard() steps — guards check access, in order
- 3. .handle() — the handler runs with the full accumulated context

If any guard denies access, the handler never runs. If middleware throws, execution stops. Steps can be interleaved — `.use(auth).guard(check).use(loadProfile).handle()`.

## Best Practices

- Use .types() — transform URL params into references. Handlers get Reference, not raw strings
- Validate early — use .body() before guards to reject invalid input fast
- Chain thoughtfully — parse → validate → guard → handle
- Keep handlers thin — delegate to services. The handler orchestrates, it doesn't contain business logic

## Next Steps

- Middleware
- Guards
- Typed Parameters
