Response Types
Send JSON, HTML, streams, and custom responses with typed status codes
JustScale's HTTP transport provides a flexible response API with type safety for different content types and status codes.
JSON Responses
res.json(data)
The most common response method sends JSON with a 200 OK status:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
export const UsersController = createController('/users', {
routes: () => ({
list: Get('/').handle(({ res }) => {
res.json({
users: [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' },
],
});
// HTTP 200 OK
// Content-Type: application/json
}),
}),
});Custom Status Codes
Use res.status(code).json(data) to send JSON with a specific status code:
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
function createUser(body: any) {
return { id: '1', name: body.name };
}
export const UsersController = createController('/users', {
routes: () => ({
// In a controller's routes
create: Post('/').handle(({ body, res }) => {
const user = createUser(body);
res.status(201).json({ user });
// HTTP 201 Created
}),
}),
});Common status codes:
200- OK (default for res.json)201- Created (after successful POST)202- Accepted (async processing)204- No Content (successful DELETE)
Empty Responses
res.status(code).end()
Send an empty response with just a status code (useful for 204, 403, 409, etc.):
import { createController } from '@justscale/core';
import { Delete } from '@justscale/http';
function deleteUser(id: string) {
console.log('Deleting user', id);
}
export const UsersController = createController('/users', {
routes: () => ({
// In a controller's routes
remove: Delete('/:id').handle(({ params, res }) => {
deleteUser(params.id);
res.status(204).end();
// HTTP 204 No Content (no body)
}),
}),
});import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
function checkUserExists(email: string) {
return email === 'exists@example.com';
}
export const UsersController = createController('/users', {
routes: () => ({
// In a controller's routes
create: Post('/').handle(({ body, res }) => {
const exists = checkUserExists(body.email);
if (exists) {
res.status(409).end();
// HTTP 409 Conflict (no body needed)
return;
}
// Create user...
}),
}),
});Error Responses
res.error(message, status?)
Send error responses with a message and optional status code (defaults to 400):
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { UserService } from '../services/user-service';
export const UsersController = createController('/users', {
inject: { users: UserService },
routes: (services) => ({
getOne: Get('/:id').handle(({ params, res }) => {
const user = services.users.findById(params.id);
if (!user) {
res.error('User not found', 404);
// HTTP 404: { "error": "User not found" }
return;
}
res.json({ user });
}),
}),
});Common error status codes:
400- Bad Request (validation error)401- Unauthorized (missing auth)403- Forbidden (insufficient permissions)404- Not Found (resource doesn't exist)409- Conflict (duplicate resource)422- Unprocessable Entity (semantic error)500- Internal Server Error
Typed Response Schemas
Use the .returns() method to declare response types. Once declared,res.status().json() and res.status().end() are type-checked against your declared schemas:
import { createController } from '@justscale/core';
import { Get, Post, Delete } from '@justscale/http';
import { populate, 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) => ({
// res.json() is typed to only accept UserResponse
getOne: Get('/:userId')
.use(populate(services.users, 'user', 'userId'))
.returns(UserResponse)
.returns(404)
.handle(({ user, res }) => {
res.json({ user }); // TypeScript: { user: { id: string, name: string, email: string } }
// res.json({ wrong: 'shape' }); // TypeScript ERROR!
}),
// res.status(201).json() typed to UserResponse
// res.status(400).json() typed to ErrorResponse
create: Post('/')
.apply(body(CreateUserBody))
.returns(UserResponse, 201)
.returns(ErrorResponse, 400)
.handle(async ({ body, res }) => {
const existing = await services.users.findByEmail(body.email);
if (existing) {
// TypeScript knows this must be ErrorResponse shape
return res.status(400).json({ error: 'Email taken' });
}
const user = await services.users.create(body);
// TypeScript knows this must be UserResponse shape
res.status(201).json({ user });
}),
// res.status(204).end() - no body required
remove: 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(); // No body needed for 204
}),
}),
});Type Safety Benefits
res.json(data)— TypeScript validates data matches the 200 response schemares.status(201).json(data)— TypeScript validates data matches the 201 response schemares.status(400).json(data)— TypeScript validates data matches the 400 error schemares.status(204).end()— Only allowed when 204 is declared (no body)- Undeclared status codes cause TypeScript errors at compile time
Info
.returns() method provides compile-time type checking and enables automatic OpenAPI schema generation.Multiple Response Types
For routes that can return different status codes, you can chain multiple .returns() calls:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
import { UserResponse, ErrorResponse } from '../schemas/user';
function findUser(id: string) {
return id === '1' ? { id, name: 'Alice' } : null;
}
export const UsersController = createController('/users', {
routes: () => ({
getOne: Get('/:id')
.returns(UserResponse) // 200 response
.returns(ErrorResponse, 404) // 404 response
.handle(({ params, res }) => {
const user = findUser(params.id);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json({ user });
}),
}),
});HTML Responses
While JustScale is primarily designed for JSON APIs, you can send HTML by manually setting headers:
import { createController } from '@justscale/core';
import { Get } from '@justscale/http';
// Note: JustScale is designed for JSON APIs
// For HTML, you'd extend the res object or use custom middleware
export const HomeController = createController('/', {
routes: () => ({
home: Get('/').handle(({ res }) => {
res.json({ message: 'Use res.json() for JSON APIs' });
}),
}),
});Warning
Streaming Responses
The current HTTP server implementation doesn't directly expose streaming APIs, but you can work around it by accessing the raw response object if needed.
Info
Response Headers
The HTTP server automatically sets common headers:
Content-Type: application/jsonfor JSON responsesAccess-Control-Allow-Origin: *for CORS
To set custom headers, you'll need to extend the response object or use middleware.
Auto-204 Behavior
If your handler doesn't send any response, JustScale automatically sends a 204 No Content:
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
function processWebhook(body: any) {
console.log('Processing webhook:', body);
}
export const WebhooksController = createController('/webhooks', {
routes: () => ({
github: Post('/github').handle(({ body }) => {
// Process webhook
processWebhook(body);
// No response sent - JustScale sends 204 No Content
}),
}),
});Info
Response Patterns
RESTful CRUD
import { createController } from '@justscale/core';
import { Get, Post, Put, Delete } from '@justscale/http';
import { UserService } from '../services/user-service';
export const UsersController = createController('/users', {
inject: { users: UserService },
routes: (services) => ({
// List: 200 with array
list: Get('/').handle(({ res }) => {
res.json({ users: services.users.findAll() });
}),
// Get One: 200 or 404
getOne: Get('/:id').handle(({ params, res }) => {
const user = services.users.findById(params.id);
if (!user) {
res.error('User not found', 404);
return;
}
res.json({ user });
}),
// Create: 201 with created resource
create: Post('/').handle(({ body, res }) => {
const user = services.users.create(body);
res.status(201).json({ user });
}),
// Update: 200 with updated resource
update: Put('/:id').handle(({ params, body, res }) => {
const user = services.users.update(params.id, body);
res.json({ user });
}),
// Delete: 204 (no content)
remove: Delete('/:id').handle(({ params, res }) => {
services.users.delete(params.id);
res.status(204).end();
}),
}),
});Conditional Responses
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
const users = {
findByEmail: (email: string) => email === 'exists@example.com' ? { id: '1' } : null,
create: (body: any) => ({ id: '2', ...body }),
};
export const UsersController = createController('/users', {
routes: () => ({
// In a controller's routes
create: Post('/').handle(({ body, res }) => {
const exists = users.findByEmail(body.email);
if (exists) {
res.status(409).json({
error: 'User already exists',
existingId: exists.id,
});
return;
}
const user = users.create(body);
res.status(201).json({ user });
}),
}),
});Partial Success
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
const users = {
create: (userData: any) => ({ id: '1', ...userData }),
};
export const UsersController = createController('/users', {
routes: () => ({
// In a controller's routes
bulkCreate: Post('/bulk').handle(({ body, res }) => {
const results = body.users.map((userData: any) => {
try {
return { success: true, user: users.create(userData) };
} catch (err) {
return { success: false, error: (err as Error).message };
}
});
const hasFailures = results.some(r => !r.success);
res.status(hasFailures ? 207 : 201).json({ results });
// 207 Multi-Status for partial success
}),
}),
});