Skip to content
HTTP Reference

Request

Official Subatom.js documentation for third-party developers.

Documentation status — public API reference

This page consolidates the detailed Subatom reference material supplied with the framework implementation examples. Treat the exported/public API surface as authoritative; internal implementation details are intentionally not presented as application APIs.

Subatom Request

The req object gives you everything you need to work with an incoming HTTP request in Subatom.

If you have used Express.js, the idea is very similar:

app.get("/users/:id", (req, res) => {
  console.log(req.params.id);
});

In Subatom, request data is available through the context object:

import { type IContext } from "subatom";

const controller = async (ctx: IContext) => {
  console.log(ctx.req.params.id);
};

The Request object is designed as a developer-friendly wrapper around Node.js's native IncomingMessage. It provides parsed request data, request metadata, HTTP helpers, body readers, and streaming methods.


Quick example

import { Router, type IContext } from "subatom";

const router = new Router();

router.get("/users/:id", {
  controller: async (ctx: IContext) => {
    const userId = ctx.req.params.id;
    const search = ctx.req.query.search;

    return ctx.res.json({
      userId,
      search,
    });
  },
});

For a request such as:

GET /users/123?search=john

you can access:

ctx.req.params.id    // "123"
ctx.req.query.search // "john"

Request Data

These properties contain data sent by the client or data added to the request during request processing.

Property Description
req.body Parsed request body
req.query Query-string parameters
req.params Route parameters
req.cookies Parsed cookies
req.user Current user attached by your application
req.locals Request-local application data
req.file A single uploaded file
req.files Uploaded files

req.body

Contains the request body.

For example, a client sends:

{
  "userName": "john doe",
  "email": "johndoe@example.com"
}

You can read it with:

const { userName, email } = ctx.req.body;

Or access individual values:

ctx.req.body.userName;
ctx.req.body.email;

With a schema

When a route has a schema, Subatom's types can infer the request body.

import { infer } from "subatom-infer";

const schema = {
  body: {
    userName: infer.string().min(2),
    email: infer.string().email(),
  },
};

router.post("/users", {
  schema,

  controller: async (ctx) => {
    const { userName, email } = ctx.req.body;

    return ctx.res.json({
      userName,
      email,
    });
  },
});

req.query

Contains values from the URL query string.

For:

GET /products?search=macBook&page=2

you can use:

const { search, page } = ctx.req.query;

console.log(search);
console.log(page);

Query values originate from the URL and are represented as strings unless your schema/inference layer resolves them to another type.

Example schema

const schema = {
  query: {
    search: infer.string().optional(),
    page: infer.number().int().min(1).optional(),
  },
};

Then:

router.get("/products", {
  schema,

  controller: async (ctx) => {
    const { search, page } = ctx.req.query;

    return ctx.res.json({
      search,
      page,
    });
  },
});

req.params

Contains route parameters.

Given this route:

router.get("/users/:userId/posts/:postId", {
  controller: async (ctx) => {
    const { userId, postId } = ctx.req.params;

    return ctx.res.json({
      userId,
      postId,
    });
  },
});

For:

GET /users/123/posts/456

the values are:

ctx.req.params.userId // "123"
ctx.req.params.postId // "456"

Route parameters are strings by default.


req.cookies

Contains cookies parsed from the request's Cookie header.

const { access_token, theme } = ctx.req.cookies;

console.log(access_token);
console.log(theme);

For example, if the client sends cookies containing:

access_token=abc123; theme=dark

you can access them through:

ctx.req.cookies.access_token;
ctx.req.cookies.theme;

req.user

req.user is application-defined user data.

Subatom does not require a specific user shape. Your authentication middleware can attach the authenticated user to the request.

const authMiddleware = async (ctx: IContext, next: NextFunction) => {
  ctx.req.user = {
    id: "usr_1001",
    role: "admin",
    email: "admin@subatomjs.dev",
  };

  await next();
};

Then a controller can read it:

router.get("/profile", {
  middleware: [authMiddleware],

  controller: async (ctx) => {
    const user = ctx.req.user;

    return ctx.res.json({
      user,
    });
  },
});

The exact shape of req.user is controlled by your application's types and middleware.


req.locals

req.locals is a place to store request-specific data while a request moves through middleware and controllers.

For example:

const traceMiddleware = async (ctx: IContext, next: NextFunction) => {
  ctx.req.locals.traceId = `subatom_${Date.now()}`;

  await next();
};

Later:

router.get("/request-info", {
  middleware: [traceMiddleware],

  controller: async (ctx) => {
    return ctx.res.json({
      traceId: ctx.req.locals.traceId,
    });
  },
});

Use locals for data that belongs to the current request and needs to be shared between middleware and the controller.


File Uploads

Subatom exposes uploaded files through req.file and req.files.

These are populated when the appropriate file middleware is used.

req.file

Use req.file for a single uploaded file.

Example route:

import { file } from "subatom";

const uploadSchema = {
  files: {
    avatar: infer.file().max(
      5 * 1024 * 1024,
      "Max file size is 5MB",
    ),
  },
};

router.post("/profile/avatar", {
  schema: uploadSchema,

  middleware: [
    file.single("avatar", {
      storage: "memory",
      allowedMimeTypes: [
        "image/webp",
        "image/jpeg",
        "image/png",
      ],
    }),
  ],

  controller: async (ctx) => {
    const uploadedFile = ctx.req.file;

    return ctx.res.json({
      fileName: uploadedFile?.filename,
      mimeType: uploadedFile?.mimetype,
      size: uploadedFile?.size,
    });
  },
});

Read the uploaded file with:

const file = ctx.req.file;

The request type exposes it as an uploaded-file value when available.


req.files

Use req.files when multiple files are uploaded.

router.post("/documents", {
  middleware: [
    file.array("documents", 5, {
      storage: "memory",
      allowedMimeTypes: [
        "image/svg",
        "image/jpeg",
        "image/png",
      ],
    }),
  ],

  controller: async (ctx) => {
    const uploadedFiles = ctx.req.files;

    return ctx.res.json({
      count: Array.isArray(uploadedFiles)
        ? uploadedFiles.length
        : 0,
    });
  },
});

With a schema, you can describe the expected files:

const schema = {
  files: {
    documents: infer
      .files()
      .min(1)
      .max(5)
      .minEach(500)
      .maxEach(2 * 1024 * 1024)
      .mime("image/*")
      .extension(["svg", "jpeg", "jpg", "png"]),
  },
};

Request Metadata

Subatom exposes common HTTP request metadata directly on req.

Property Description
req.raw Native Node.js IncomingMessage
req.method HTTP method
req.url Request URL
req.path URL pathname
req.headers Request headers
req.protocol http or https
req.host Request host
req.hostname Hostname without the port
req.ip Resolved client IP
req.secure Whether the request uses HTTPS

req.raw

The original Node.js request object.

const nativeRequest = ctx.req.raw;

Its type is:

IncomingMessage

Use req.raw when you need functionality provided directly by Node.js that is not exposed by the Subatom request API.


req.method

The HTTP method.

ctx.req.method;

Examples:

GET
POST
PUT
PATCH
DELETE

The request implementation normalizes the method to uppercase.


req.url

The request URL.

ctx.req.url;

Example:

/users/123?active=true

req.path

The pathname portion of the URL.

ctx.req.path;

For:

/users/123?active=true

the path is:

/users/123

req.headers

Contains the request headers.

const contentType = ctx.req.headers["content-type"];
const userAgent = ctx.req.headers["user-agent"];

The type allows:

string | string[] | undefined

for header values.

For most common header access, prefer req.get().


req.protocol

The protocol used by the request:

ctx.req.protocol;

It is:

"http" | "https"

req.host

The resolved request host.

ctx.req.host;

req.hostname

The hostname without the port.

ctx.req.hostname;

For example, a host such as:

example.com:3000

has a hostname of:

example.com

req.ip

The resolved client IP address.

ctx.req.ip;

Proxy handling can be enabled through RequestOptions.trustProxy or the SUBATOM_TRUST_PROXY environment setting.


req.secure

A boolean indicating whether the request uses HTTPS.

if (ctx.req.secure) {
  // HTTPS request
}

Sessions

Subatom's request object also exposes session information.

req.session

Access the current session:

const session = ctx.req.session;

req.sessionID

Access the current session ID:

const sessionId = ctx.req.sessionID;

The request types expose these as ISession and string respectively.


HTTP Helper Methods

req.get(name)

Reads a request header.

const apiKey = ctx.req.get("x-api-key");
const userAgent = ctx.req.get("user-agent");

It returns:

string | undefined

If the header does not exist, the result is undefined.

Example:

const apiKey = ctx.req.get("x-api-key");

if (!apiKey) {
  return ctx.res.status(401).json({
    error: "API key is required",
  });
}

req.accepts()

Checks the request's Accept header.

Check one type

When one string is passed, it returns a boolean:

const acceptsJson = ctx.req.accepts("application/json");

if (acceptsJson) {
  // Client accepts JSON
}

Return type:

boolean

Check multiple types

When multiple types are provided, it returns the first matching type or false.

const bestMatch = ctx.req.accepts(
  "text/html",
  "application/json",
);

You can also pass an array:

const bestMatch = ctx.req.accepts([
  "text/html",
  "application/json",
]);

Return type:

string | false

Reading the Request Body

Subatom provides methods for reading the raw request stream in different formats.

Method Returns
req.text() Promise<string>
req.json() Promise<T>
req.formData() Promise<URLSearchParams>
req.buffer() Promise<Buffer>

Each method accepts an optional byte limit:

await ctx.req.text(limitInBytes);
await ctx.req.json(limitInBytes);
await ctx.req.formData(limitInBytes);
await ctx.req.buffer(limitInBytes);

The limit is useful when you want to restrict how much request data can be read.


req.text()

Reads the request body as text.

const rawText = await ctx.req.text();

return ctx.res.send(
  `Received text (${rawText.length} bytes): ${rawText}`,
);

You can provide a maximum size:

const rawText = await ctx.req.text(
  1024 * 1024,
);

The example above limits the read to 1 MB.


req.json()

Reads the request body as JSON.

const data = await ctx.req.json();

return ctx.res.json({
  received: data,
});

You can provide a type parameter:

type User = {
  name: string;
  email: string;
};

const user = await ctx.req.json<User>();

console.log(user.name);
console.log(user.email);

You can also provide a byte limit:

const data = await ctx.req.json<MyBody>(
  1024 * 1024,
);

req.formData()

Reads the request body as form data.

const form = await ctx.req.formData();

const title = form.get("title");
const active = form.get("active");

The method returns:

URLSearchParams

If a validator has already consumed the request body and populated req.body, use req.body instead of attempting to read the stream again.


req.buffer()

Reads the request body as a Buffer.

const buffer = await ctx.req.buffer();

console.log(buffer.byteLength);

You can specify a maximum size:

const buffer = await ctx.req.buffer(
  1024 * 1024,
);

The example limits the request body to 1 MB.

This is useful when you need the raw bytes of the request body.


Request Streams

For large payloads or streaming workloads, you can work directly with the request stream instead of loading the entire body into memory.

Method Purpose
req.onData() Listen for incoming data chunks
req.onEnd() Listen for the end of the request
req.pipe() Pipe the request into a writable stream
req.stream() Get a readable stream

req.onData()

Runs a callback whenever a request data chunk arrives.

const unbind = ctx.req.onData((chunk) => {
  console.log(chunk);
});

The method returns an unsubscribe function:

unbind();

Example:

let chunks = 0;
let totalBytes = 0;

const unbind = ctx.req.onData((chunk: Buffer) => {
  chunks++;
  totalBytes += chunk.length;
});

ctx.req.onEnd(() => {
  unbind();

  console.log({
    chunks,
    totalBytes,
  });
});

req.onEnd()

Runs a callback when the request stream ends.

ctx.req.onEnd(() => {
  console.log("Request finished");
});

It also returns an unsubscribe function:

const unbind = ctx.req.onEnd(() => {
  // ...
});

unbind();

req.pipe()

Pipes the incoming request into a writable stream.

import { createWriteStream } from "node:fs";

const destination = createWriteStream(
  "./uploaded.bin",
);

ctx.req.pipe(destination);

Because pipe() uses Node.js streams, it is useful for processing large request bodies without first loading the complete body into memory.

Example:

const destination = createWriteStream(
  "./streamed_output.bin",
);

await new Promise((resolve, reject) => {
  destination.on("finish", resolve);
  destination.on("error", reject);

  ctx.req.pipe(destination);
});

req.stream()

Returns the request as a Node.js readable stream.

const stream = ctx.req.stream();

stream.on("data", (chunk) => {
  console.log(chunk);
});

stream.on("end", () => {
  console.log("Request finished");
});

A practical example is calculating a hash while the request is streamed:

import { createHash } from "node:crypto";

const stream = ctx.req.stream();
const hash = createHash("sha256");

stream.on("data", (chunk: Buffer) => {
  hash.update(chunk);
});

stream.on("end", () => {
  console.log(hash.digest("hex"));
});

Request vs Context

In a Subatom controller, you normally receive a Context object:

controller: async (ctx) => {
  // ...
}

The request is available as:

ctx.req

So:

ctx.req.body
ctx.req.query
ctx.req.params
ctx.req.headers

The Context also provides convenient request aliases:

ctx.body
ctx.query
ctx.params
ctx.headers
ctx.cookies
ctx.file
ctx.files
ctx.user
ctx.locals

For example:

// Explicit request access
const userId = ctx.req.params.userId;

// Context shortcut
const sameUserId = ctx.params.userId;

The context facade maps these values back to the underlying request.

You can also access the underlying request and response through:

ctx.request
ctx.response

ctx.request is an alias for ctx.req, while ctx.response is an alias for ctx.res.


TypeScript

The request API is generic, so request data can be strongly typed.

The core request type accepts generic types for:

IRequest<
  Body,
  Query,
  Params,
  Cookies,
  User,
  Locals,
  Ip,
  Protocol,
  Secure,
  Hostname,
  Path,
  Files
>

When a route schema is supplied, the context type can infer types for body, query, params, headers, and files.

For example:

const schema = {
  body: {
    name: infer.string(),
    age: infer.number(),
  },

  query: {
    page: infer.number().optional(),
  },

  params: {
    id: infer.string(),
  },
};

The controller can then use the inferred request properties:

router.get("/users/:id", {
  schema,

  controller: async (ctx) => {
    const id = ctx.req.params.id;
    const page = ctx.req.query.page;

    // ...
  },
});

Subatom's context inference supports standard-schema-style output types and several common schema inference patterns. The exact runtime validation behavior depends on the schema/validator used by the route.


Complete Example

Here is a small example showing several request features together:

import {
  Router,
  type IContext,
  type NextFunction,
} from "subatom";
import { infer } from "subatom-infer";

const router = new Router();

const schema = {
  body: {
    name: infer.string().min(2),
    email: infer.string().email(),
  },

  query: {
    page: infer.number().int().min(1).optional(),
  },

  params: {
    userId: infer.string().min(1),
  },

  headers: {
    "user-agent": infer.string().optional(),
  },
};

const middleware = async (
  ctx: IContext,
  next: NextFunction,
) => {
  ctx.req.locals.requestId =
    `req_${Date.now()}`;

  await next();
};

router.post("/users/:userId", {
  schema,
  middleware: [middleware],

  controller: async (ctx) => {
    const {
      userId,
    } = ctx.req.params;

    const {
      name,
      email,
    } = ctx.req.body;

    const page =
      ctx.req.query.page;

    const userAgent =
      ctx.req.get("user-agent");

    return ctx.res.json({
      userId,
      name,
      email,
      page,
      userAgent,
      requestId:
        ctx.req.locals.requestId,
    });
  },
});

export default router;

API Reference

Request Data

req.body
req.query
req.params
req.cookies
req.user
req.locals
req.file
req.files

Request Metadata

req.raw
req.method
req.url
req.path
req.headers
req.protocol
req.host
req.hostname
req.ip
req.secure
req.session
req.sessionID

HTTP Helpers

req.get()
req.accepts()

Body Readers

req.text()
req.json()
req.formData()
req.buffer()

Stream Methods

req.onData()
req.onEnd()
req.pipe()
req.stream()

When should I use each method?

Need normal JSON/body data?

Use:

ctx.req.body

Need query parameters?

Use:

ctx.req.query

Need a route parameter?

Use:

ctx.req.params

Need a header?

Use:

ctx.req.get("header-name")

Need to read raw text?

Use:

await ctx.req.text()

Need raw bytes?

Use:

await ctx.req.buffer()

Need JSON directly from the request stream?

Use:

await ctx.req.json()

Need form data?

Use:

await ctx.req.formData()

Need to process a large request without loading it all into memory?

Use:

ctx.req.stream()

or:

ctx.req.pipe(destination)

Need to inspect individual chunks?

Use:

ctx.req.onData(...)

and:

ctx.req.onEnd(...)

Summary

The Subatom req object gives you a single, consistent API for working with HTTP requests:

ctx.req.body
ctx.req.query
ctx.req.params
ctx.req.cookies

ctx.req.headers
ctx.req.method
ctx.req.url
ctx.req.path
ctx.req.ip
ctx.req.protocol

ctx.req.get(...)
ctx.req.accepts(...)

await ctx.req.text()
await ctx.req.json()
await ctx.req.formData()
await ctx.req.buffer()

ctx.req.onData(...)
ctx.req.onEnd(...)
ctx.req.pipe(...)
ctx.req.stream()

For most application code, you will primarily use body, query, params, headers, cookies, user, and locals. Use the body-reader and stream APIs when you need direct control over the incoming request data.