Skip to content

Responses

GamanJS provides a global Res (alias for the Responder class) to create consistent and standardized responses. The goal is for all API responses from your application to have the same format — no more inconsistent response formats across endpoints.

All responses created via Res.send() / Res.message() / Res.error() produce a consistent JSON format:

{
"success": true,
"message": "Success",
"data": { ... },
"metadata": {
"requestId": "abc123...",
"timestamp": "2026-03-23T07:00:00.000Z"
}
}
FieldTypeDescription
successbooleantrue if status 2xx, false otherwise
messagestringResponse message (auto-set based on status code, or custom)
dataanyResponse data (optional)
errorsRecord<string, string[]>Validation errors (optional)
metadataobjectRequest ID & timestamp, plus custom metadata

The primary method for sending responses with data. This is the most commonly used.

// Send data with status 200 (default)
return Res.send({ name: 'Angga', age: 25 });
// Send data with custom status
return Res.send(users, 201);
// Send data with full options
return Res.send(users, {
status: 200,
message: 'Users retrieved successfully',
});

Output:

{
"success": true,
"message": "Success",
"data": { "name": "Angga", "age": 25 },
"metadata": {
"requestId": "...",
"timestamp": "..."
}
}

Send a response with only a message (no data):

return Res.message('Welcome to GamanJS');
return Res.message('User created successfully', 201);

Output:

{
"success": true,
"message": "Welcome to GamanJS",
"metadata": { ... }
}

Send an error response with validation error details:

return Res.error({
email: ['Email is required', 'Invalid email format'],
password: ['Password must be at least 8 characters'],
}, 422);

Output:

{
"success": false,
"message": "Unprocessable Entity",
"errors": {
"email": ["Email is required", "Invalid email format"],
"password": ["Password must be at least 8 characters"]
},
"metadata": { ... }
}

Shortcut for Res.send(data) with status 200:

return Res.ok(users);

Shortcut for 404 response:

return Res.notFound();

Methods for sending responses without the standard format (raw body):

Send raw JSON (without success, message wrapper, etc):

return Res.json({ custom: 'format' });

Send plain text:

return Res.text('Hello World');

Send HTML:

return Res.html('<h1>Hello World</h1>');

Send stream response:

const file = Bun.file('./large-video.mp4');
return Res.stream(file.stream());

Send HTML response from a view template:

return Res.render('home', { title: 'Welcome' });

Redirect response:

return Res.redirect('/dashboard'); // 302
return Res.redirect('/new-url', 301); // 301

All Responder instances support chaining:

return Res.send(data)
.status(201)
.message('User created')
.meta({ page: 1, total: 100 })
.header('X-Custom', 'value');
MethodDescription
.message(msg)Set custom message
.status(code)Set HTTP status code
.statusText(text)Set status text
.meta(data)Add custom metadata
.header(key, value)Add response header
.error(errors, msg?)Set validation errors

Shortcut methods for setting status codes:

return Res.send(data).ok(); // 200
return Res.send(data).created(); // 201
return Res.send(data).accepted(); // 202
return Res.send(data).noContent(); // 204
return Res.send(data).badRequest(); // 400
return Res.send(data).unauthorized(); // 401
return Res.send(data).forbidden(); // 403
return Res.send(data).notFound(); // 404
return Res.send(data).methodNotAllowed(); // 405
return Res.send(data).tooManyRequests(); // 429
return Res.send(data).internalServerError();// 500
return Res.send(null).movedPermanently('/new-url'); // 301
return Res.send(null).movedTemporarily('/temp-url'); // 302

import { composeController } from 'gaman/compose';
import UserService from '../services/UserService';
import type { RT } from 'gaman/types';
export default composeController(
(userService: RT<typeof UserService> = UserService()) => ({
// GET /users
GetAll(ctx) {
const users = userService.findAll();
return Res.send(users).meta({ total: users.length });
},
// GET /users/:id
GetById(ctx) {
const user = userService.findById(ctx.param('id'));
if (!user) return Res.message('User not found').notFound();
return Res.send(user);
},
// POST /users
async Create(ctx) {
const body = await ctx.json();
if (!body.email) {
return Res.error({ email: ['Email is required'] }, 422);
}
const user = userService.create(body);
return Res.send(user, { status: 201, message: 'User created' });
},
// DELETE /users/:id
Delete(ctx) {
userService.delete(ctx.param('id'));
return Res.send(null).noContent(); // 204 No Content
},
}),
);

Problem: Most developers create API responses with different formats across endpoints. Some use { data: ... }, { result: ... }, { status: 'ok' }, etc.

Solution: GamanJS enforces consistent response formatting through Res. All responses automatically have success, message, data, errors, and metadata.