Skip to main content

Request & Response Primitives

Zelt provides a request() primitive for accessing request data and a response() primitive for building responses. request() can be used as a default parameter in controller methods and returns a request accessor.

Request Primitives​

Query Parameters​

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/search')
export class SearchController {
  @Get('/')
  search(req = request(), res = response()) {
    const q = req.queryParam('q');
    const tags = req.queryParams('tag');
    // q: string | undefined
    // tags: string[] (empty array if not provided)
    return res.json({ query: q, tags });
  }
}
MethodReturn TypeDescription
req.queryParam(name)string | undefinedGet a single query parameter
req.queryParams(name)string[]Get all values for a query parameter

Headers​

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/api')
export class ApiController {
  @Get('/info')
  info(req = request(), res = response()) {
    const userAgent = req.header('User-Agent');
    const acceptLanguage = req.header('Accept-Language');
    return res.json({ userAgent, acceptLanguage });
  }
}
MethodReturn TypeDescription
req.header(name)string | undefinedGet a request header value

Cookies​

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/session')
export class SessionController {
  @Get('/')
  getSession(req = request(), res = response()) {
    const sessionId = req.cookie('session_id');
    return res.json({ sessionId });
  }
}
MethodReturn TypeDescription
req.cookie(name)string | undefinedGet a cookie value

URL & Path​

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/debug')
export class DebugController {
  @Get('/request')
  requestInfo(req = request(), res = response()) {
    const fullUrl = req.url();
    const requestPath = req.path();
    const httpMethod = req.method();
    return res.json({
      url: fullUrl,      // "http://localhost:3000/debug/request?foo=bar"
      path: requestPath, // "/debug/request"
      method: httpMethod // "GET"
    });
  }
}
MethodReturn TypeDescription
req.url()stringFull request URL including query string
req.path()stringRequest path without query string
req.method()stringHTTP method (GET, POST, etc.)

Client IP​

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/debug')
export class DebugController {
  @Get('/ip')
  clientIp(req = request(), res = response()) {
    const ip = req.ip();
    return res.json({ ip });
  }
}
MethodReturn TypeDescription
req.ip()string | undefinedClient IP address

Request Body​

The request body target is configured when calling request(). Use await req.body() to read the parsed body. When no schema is passed, request() uses an internal any schema and the default json target.

import { Controller, Post, request, response } from '@zeltjs/core';
import * as v from 'valibot';

const FormSchema = v.record(v.string(), v.unknown());

@Controller('/upload')
export class UploadController {
  @Post('/json')
  async uploadJson(req = request(), res = response()) {
    const data = await req.body();
    return res.json({ received: data });
  }

  @Post('/form')
  async uploadForm(req = request(FormSchema, { target: 'form' }), res = response()) {
    const formData = await req.body();
    return res.json({ fields: formData });
  }
}
request() callawait req.body() typeDescription
request()unknownParsed JSON body with the default any schema
request(schema)schema outputValidated JSON body
request(schema, { target: 'form' })schema outputValidated form data
tip

For validated request bodies with automatic type inference, use request() with a schema instead.

Path Parameters​

import { Controller, Get, request, response } from '@zeltjs/core';

@Controller('/users')
export class UserController {
  @Get('/:id')
  getUser(req = request(), res = response()) {
    const id = req.pathParam('id');
    // id: string (throws if not defined)
    return res.json({ userId: id });
  }
}
MethodReturn TypeDescription
req.pathParam(name)stringGet a path parameter (throws if undefined)

Response Primitives​

response()​

The response() primitive returns a builder for constructing HTTP responses:

import { Controller, Get, Post, response } from '@zeltjs/core';

@Controller('/api')
export class ApiController {
  @Get('/data')
  getData(res = response()) {
    return res.json({ message: 'Hello' });
  }

  @Get('/redirect')
  redirect(res = response()) {
    return res.redirect('/new-location', 302);
  }

  @Get('/text')
  getText(res = response()) {
    return res.text('Plain text response');
  }

  @Post('/created')
  create(res = response()) {
    return res.json({ id: '123' }, 201);
  }
}

Response Methods​

MethodDescription
status(code)New builder retaining the literal status type
empty(status?)Reply without a body, including 204/304
json(data, status?, headers?)JSON response with optional status code and headers
text(data, status?)Plain text response
redirect(url, status?)HTTP redirect (default: 302)
body(data, status?)Raw body response
header(name, value)Set a response header (chainable)
stream(cb, onError?)Stream binary data
streamText(cb, onError?)Stream text data
sse(cb, onError?)Server-Sent Events stream

Setting Cookies​

import { Controller, Post, response } from '@zeltjs/core';

@Controller('/auth')
export class AuthController {
  @Post('/login')
  login(res = response()) {
    return res
      .setCookie('session_id', 'abc123', {
        httpOnly: true,
        secure: true,
        sameSite: 'Strict',
        maxAge: 60 * 60 * 24, // 1 day
      })
      .json({ success: true });
  }

  @Post('/logout')
  logout(res = response()) {
    return res
      .deleteCookie('session_id')
      .json({ success: true });
  }
}
OptionTypeDescription
domainstringCookie domain
expiresDateExpiration date
httpOnlybooleanHTTP-only flag
maxAgenumberMax age in seconds
pathstringCookie path
securebooleanSecure flag
sameSite'Strict' | 'Lax' | 'None'SameSite attribute

Streaming Responses​

Zelt provides streaming capabilities for real-time data delivery.

Basic Streaming​

Use stream() for binary data or streamText() for text data:

import { Controller, Get, response } from '@zeltjs/core';

@Controller('/stream')
export class StreamController {
  @Get('/data')
  streamData(res = response()) {
    return res.stream(async (stream) => {
      await stream.write('chunk 1');
      await stream.sleep(100);
      await stream.write('chunk 2');
      await stream.close();
    });
  }

  @Get('/lines')
  streamLines(res = response()) {
    return res.streamText(async (stream) => {
      await stream.writeln('line 1');
      await stream.writeln('line 2');
      await stream.close();
    });
  }
}

Server-Sent Events (SSE)​

Use sse() for Server-Sent Events:

import { Controller, Get, response } from '@zeltjs/core';

@Controller('/events')
export class EventController {
  @Get('/updates')
  streamUpdates(res = response()) {
    return res.sse(async (stream) => {
      await stream.writeSSE({ data: 'connected', event: 'open' });

      for (let i = 0; i < 5; i++) {
        await stream.sleep(1000);
        await stream.writeSSE({
          data: JSON.stringify({ count: i }),
          event: 'update',
          id: String(i),
        });
      }

      await stream.close();
    });
  }
}

Stream Writer Methods​

MethodDescription
write(input)Write Uint8Array or string to stream
writeln(input)Write string with newline
writeSSE(message)Write SSE message (SSE streams only)
sleep(ms)Pause for specified milliseconds
pipe(body)Pipe a ReadableStream
close()Close the stream
abort()Abort the stream
onAbort(listener)Register abort handler

SSE Message Format​

type SSEMessage = {
  data: string | Promise<string>;
  event?: string;
  id?: string;
  retry?: number;
};

Error Handling​

Both streaming methods accept an optional error handler:

@Controller('/stream')
class StreamController {
  @Get('/data')
  streamData(res = response()) {
    return res.stream(
      async (stream) => {
        // ... stream logic
      },
      async (error, stream) => {
        await stream.write(`Error: ${error.message}`);
        await stream.close();
      }
    );
  }
}

Chaining Response Methods​

Builder methods (status, header, setCookie, deleteCookie) return a new builder without changing the original. Use the returned builder when chaining:

@Controller('/files')
class FileController {
  @Get('/download')
  download(res = response()) {
    return res
      .header('Content-Disposition', 'attachment; filename="report.csv"')
      .header('Cache-Control', 'no-cache')
      .setCookie('download_started', 'true')
      .text('id,name\n1,Alice\n2,Bob');
  }
}

Headers in middleware​

Use responseHeaders() for request-scoped header effects. Unlike the immutable response() builder, this accessor changes the running HTTP response.

import { Middleware, responseHeaders } from '@zeltjs/core';
import type { Next } from '@zeltjs/core';

@Middleware
class HeaderMiddleware {
  async use(next: Next, headers = responseHeaders()) {
    headers.set('X-Example', 'yes');
    await next();
  }
}

response().header(...) only describes a reply. Discarding that builder does not set a header on a later bare return value.