Request Handling

Work with HTTP requests and responses in JustScale

HTTP routes receive a context object containing request data (params, body,query, headers) and a response helper (res).

The Request Context

Every HTTP route handler receives a context object with transport-specific properties:

src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';

export const UsersController = createController('/users', {
  routes: () => ({
    getOne: Get('/:id').handle(({ params, body, query, headers, res }) => {
      // params: Route parameters extracted from the URL path
      // body: Parsed JSON request body (POST/PUT/PATCH)
      // query: Query string parameters
      // headers: Request headers
      // res: Response helper for sending responses
    }),
  }),
});

The Response Object

The res object provides methods for sending HTTP responses. Always pair with .returns() to declare response types:

res.json(data)

Send a JSON response with 200 status:

src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { UsersResponse } from '../schemas/user';

export const UsersController = createController('/users', {
  routes: () => ({
    list: Get('/')
      .returns(UsersResponse)
      .handle(({ res }) => {
        res.json({ users: [{ id: '1', name: 'Alice' }] });
        // HTTP 200 with Content-Type: application/json
      }),
  }),
});

res.status(code).json(data)

Send a JSON response with a custom status code:

src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
import { body } from '@justscale/http/builder';
import { UserService } from '../services/user-service';
import { UserResponse, ErrorResponse, CreateUserBody } from '../schemas/user';

export const UsersController = createController('/users', {
  inject: { users: UserService },
  routes: (services) => ({
    create: Post('/')
      .apply(body(CreateUserBody))
      .returns(UserResponse, 201)
      .returns(ErrorResponse, 400)
      .handle(async ({ body, res }) => {
        const user = await services.users.create(body);
        res.status(201).json({ user });
        // HTTP 201 Created
      }),
  }),
});

res.status(code).end()

Send an empty response (for void responses like 204):

src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Delete } from '@justscale/http';
import { populate } from '@justscale/http';
import { UserService } from '../services/user-service';

export const UsersController = createController('/users', {
  inject: { users: UserService },
  routes: (services) => ({
    delete: Delete('/:userId')
      .use(populate(services.users, 'user', 'userId'))
      .returns(204)
      .returns(404)
      .handle(async ({ user, res }) => {
        await services.users.delete(user.id);
        res.status(204).end();
        // HTTP 204 No Content
      }),
  }),
});

res.error(message, status?)

Send an error response (default 400):

src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { UserResponse, ErrorResponse } from '../schemas/user';

export const UsersController = createController('/users', {
  routes: () => ({
    getOne: Get('/:id')
      .returns(UserResponse)
      .returns(ErrorResponse, 400)
      .returns(404)
      .handle(({ params, res }) => {
        if (!params.id) {
          return res.status(400).json({ error: 'User ID is required' });
        }
        // Continue processing...
      }),
  }),
});

Headers

Access request headers through the headers object:

src/controllers/profile.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';

export const ProfileController = createController('/profile', {
  routes: () => ({
    getProfile: Get('/').handle(({ headers, res }) => {
      const authHeader = headers.authorization;
      const userAgent = headers['user-agent'];

      if (!authHeader) {
        res.error('Authorization header required', 401);
        return;
      }

      res.json({ authenticated: true });
    }),
  }),
});
ℹ️

Info

Header names are lowercase in the headers object.

Cookies

Cookies are available in the headers.cookie string. You can parse them manually or use a middleware:

src/controllers/session.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';

export const SessionController = createController('/', {
  routes: () => ({
    checkSession: Get('/check-session').handle(({ headers, res }) => {
      const cookieHeader = headers.cookie;
      // cookieHeader = "session=abc123; theme=dark"

      // Parse cookies manually
      const cookies = Object.fromEntries(
        cookieHeader?.split('; ').map(c => c.split('=')) ?? []
      );

      res.json({ sessionId: cookies.session });
    }),
  }),
});

Auto-Response Handling

If your handler doesn't send a response, JustScale automatically sends a 204 No Content:

src/controllers/health.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';

export const HealthController = createController('/', {
  routes: () => ({
    ping: Get('/ping').handle(() => {
      // No response sent - JustScale sends 204 No Content
    }),
  }),
});
⚠️

Warning

Always ensure you send a response or throw an error. Silent failures can be confusing!

Error Handling

If a handler throws an error, JustScale catches it and sends a 500 response:

src/controllers/risky.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { DataResponse } from '../schemas/data';

export const RiskyController = createController('/', {
  routes: () => ({
    risky: Get('/risky')
      .returns(DataResponse)
      .returns(500)  // Document that this can fail
      .handle(({ res }) => {
        throw new Error('Something went wrong!');
        // JustScale sends: {"error": "Something went wrong!"} with status 500
      }),
  }),
});

For controlled error responses, declare them with .returns():

src/controllers/users.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { populate } from '@justscale/http';
import { UserService } from '../services/user-service';
import { UserResponse, ErrorResponse } from '../schemas/user';

export const UsersController = createController('/users', {
  inject: { users: UserService },
  routes: (services) => ({
    // With populate middleware
    getOne: Get('/:userId')
      .use(populate(services.users, 'user', 'userId'))
      .returns(UserResponse)
      .returns(404)  // populate handles this automatically
      .handle(({ user, res }) => {
        res.json({ user });
      }),

    // Or with manual error handling:
    getOneManual: Get('/manual/:id')
      .returns(UserResponse)
      .returns(ErrorResponse, 404)
      .handle(async ({ params, res }) => {
        const user = await services.users.findById(params.id);
        if (!user) {
          return res.status(404).json({ error: 'User not found' });
        }
        res.json({ user });
      }),
  }),
});

Multiple Responses

⚠️

Warning

Only the first response is sent. Subsequent calls to res.json() orres.error() are ignored:
src/controllers/example.tsTypeScript
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';

export const ExampleController = createController('/', {
  routes: () => ({
    example: Get('/example').handle(({ res }) => {
      res.json({ message: 'First' });
      res.json({ message: 'Second' }); // Ignored - response already sent
    }),
  }),
});