Skip to main content
Version: v1 (current)

Response Object

Fastay provides a flexible response system that allows route handlers to return data in multiple formats. The framework automatically handles HTTP response creation, content-type headers, and serialization based on the returned value.

Response Types

Fastay supports several response formats through its unified response interface:

interface FastayResponse {
status?: number;
body?: any;
redirect?: string;
headers?: Record<string, string>;
cookies?: Record<string, FastayCookie>;
file?: FastayFile;
stream?: NodeJS.ReadableStream;
raw?: Buffer | string;
static?: FastayStatic;
}

interface FastayCookie {
value: string;
options?: Record<string, any>;
}

interface FastayFile {
path: string;
filename?: string;
options?: any;
}

interface FastayStatic {
path: string;
contentType?: string;
}

Simple Return Values

Route handlers can return values directly without explicit response configuration. Fastay automatically infers the appropriate HTTP response.

JSON Objects (Default)

Returning an object automatically sets Content-Type: application/json:

export async function GET() {
return { message: "Hello", count: 42 };
}
// Response: 200 OK, Content-Type: application/json
// Body: {"message":"Hello","count":42}

Strings

String returns become plain text responses:

export async function GET() {
return "Hello World";
}
// Response: 200 OK, Content-Type: text/plain
// Body: "Hello World"

Numbers

Numbers are converted to strings:

export async function GET() {
return 42;
}
// Response: 200 OK, Content-Type: text/plain
// Body: "42"

Arrays

Arrays are serialized as JSON:

export async function GET() {
return [1, 2, 3, 4, 5];
}
// Response: 200 OK, Content-Type: application/json
// Body: [1,2,3,4,5]

Null or Undefined

Null or undefined returns generate a 204 No Content response:

export async function DELETE() {
// Delete resource
return null;
}
// Response: 204 No Content, no body

Advanced Response Configuration

For complete control, return a response configuration object:

Status Codes

export async function POST(request: Request) {
const data = await request.body;

return {
status: 201, // Created
body: {
success: true,
data: data,
message: "Resource created",
},
};
}

Custom Headers

export async function GET() {
return {
status: 200,
body: { data: "value" },
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "custom-value",
"Cache-Control": "public, max-age=3600",
ETag: '"abc123"',
},
};
}

Cookies

Set cookies with configurable options:

export async function POST(request: Request) {
const token = generateAuthToken();

return {
status: 200,
body: { message: "Login successful" },
cookies: {
auth_token: {
value: token,
options: {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: "strict",
path: "/",
domain:
process.env.NODE_ENV === "production" ? ".example.com" : undefined,
},
},
user_prefs: {
value: "dark_mode",
options: {
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
},
},
},
};
}
OptionTypeDefaultDescription
httpOnlybooleanfalsePrevent JavaScript access
securebooleanfalseOnly send over HTTPS
maxAgenumber(session)Lifetime in milliseconds
sameSitestring"lax""strict", "lax", or "none"
pathstring"/"Cookie path scope
domainstringCurrent domainDomain scope

Special Response Types

File Downloads

Serve files with download prompts:

export async function GET() {
const filePath = "/path/to/document.pdf";

return {
file: {
path: filePath,
filename: "monthly-report.pdf",
contentType: "application/pdf",
},
};
}

File Options

OptionTypeDefaultDescription
pathstringRequiredFile system path
filenamestringOriginal filenameDownload filename
contentTypestringInferredMIME type
optionsobject{}Additional fs options

Redirects

Perform HTTP redirects:

// Temporary redirect (302 Found)
export async function GET() {
return {
redirect: "/new-location",
status: 302,
};
}

// Permanent redirect (301 Moved Permanently)
export async function GET() {
return {
redirect: "https://new-domain.com/api/v2",
status: 301,
};
}

// Redirect with query parameters
export async function GET() {
return {
redirect: "/search?q=fastay&sort=newest",
status: 302,
};
}

Stream Responses

Stream large responses for efficient memory usage:

import fs from "fs";

export async function GET() {
const videoPath = "/path/to/video.mp4";

return {
stream: fs.createReadStream(videoPath),
headers: {
"Content-Type": "video/mp4",
"Content-Length": fs.statSync(videoPath).size.toString(),
"Accept-Ranges": "bytes",
"Cache-Control": "public, max-age=31536000",
},
};
}

Raw Responses

Send raw buffers or strings with custom encoding:

export async function GET() {
return {
raw: Buffer.from("Hello World in binary"),
headers: {
"Content-Type": "application/octet-stream",
},
};
}

export async function GET() {
return {
raw: "<h1>Hello World</h1>",
headers: {
"Content-Type": "text/html",
},
};
}

Static File Serving

Serve static files with proper headers:

export async function GET() {
return {
static: {
path: "/path/to/image.png",
contentType: "image/png",
},
};
}

Response Methods (Express Underlying)

While Fastay's response system handles most cases, you can access Express's underlying response methods through middleware or advanced scenarios:

Setting Status Codes

export async function handler(request: Request, response: Response) {
response.status(201); // Set status code
return { message: "Created" };
}

Setting Headers

export async function handler(request: Request, response: Response) {
response.setHeader("X-Custom-Header", "value");
response.set({
"Content-Type": "application/json",
"Cache-Control": "no-cache",
});
return { data: "value" };
}

Setting Cookies

export async function handler(request: Request, response: Response) {
response.cookie("session", "token123", {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
});

response.clearCookie("old_session");

return { message: "Cookie set" };
}

Content Negotiation

Fastay can handle different response formats based on Accept headers:

export async function GET(request: Request) {
const data = { message: "Hello", timestamp: new Date() };

if (request.accepts("html")) {
return {
raw: `<h1>${data.message}</h1><p>${data.timestamp}</p>`,
headers: { "Content-Type": "text/html" },
};
}

if (request.accepts("xml")) {
return {
raw: `<response><message>${data.message}</message></response>`,
headers: { "Content-Type": "application/xml" },
};
}

// Default to JSON
return data;
}

Error Responses

Return error responses with appropriate status codes:

export async function GET(request: Request) {
const resource = await findResource(request.params.id);

if (!resource) {
return {
status: 404,
body: {
error: "Resource not found",
message: `Resource with ID ${request.params.id} does not exist`,
code: "NOT_FOUND",
},
};
}

if (!hasPermission(request.user, resource)) {
return {
status: 403,
body: {
error: "Forbidden",
message: "You don't have permission to access this resource",
code: "FORBIDDEN",
},
};
}

return resource;
}

Response Compression

Fastay automatically handles response compression when configured:

// Enable compression in createApp configuration
await createApp({
expressOptions: {
middlewares: [compression()],
},
});

// Responses are automatically compressed based on:
// - Content-Type
// - Response size
// - Accept-Encoding header

Caching Headers

Set appropriate caching headers:

export async function GET() {
const data = await getCachableData();

return {
body: data,
headers: {
"Cache-Control": "public, max-age=300", // 5 minutes
ETag: generateETag(data),
"Last-Modified": new Date().toUTCString(),
},
};
}

Common Response Patterns

Paginated Responses

export async function GET(request: Request) {
const { page = "1", limit = "20" } = request.query;
const pageNum = parseInt(page as string);
const limitNum = parseInt(limit as string);

const [items, total] = await Promise.all([
getItems(pageNum, limitNum),
getTotalCount(),
]);

return {
body: {
items,
pagination: {
page: pageNum,
limit: limitNum,
total,
totalPages: Math.ceil(total / limitNum),
hasNext: pageNum < Math.ceil(total / limitNum),
hasPrev: pageNum > 1,
},
},
headers: {
"X-Total-Count": total.toString(),
"X-Total-Pages": Math.ceil(total / limitNum).toString(),
},
};
}

API Versioning in Responses

export async function GET(request: Request) {
const data = { id: 1, name: "Example" };
const apiVersion = request.get("X-API-Version") || "v1";

if (apiVersion === "v2") {
return {
body: {
data,
meta: {
version: "v2",
format: "enveloped",
},
},
};
}

// v1 format (direct data)
return data;
}

File Upload Response

export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("file") as File;

const fileUrl = await saveFile(file);

return {
status: 201,
body: {
message: "File uploaded successfully",
file: {
originalName: file.name,
size: file.size,
type: file.type,
url: fileUrl,
uploadedAt: new Date().toISOString(),
},
},
headers: {
Location: fileUrl,
},
};
}

Response Validation

Fastay validates response structures and provides helpful errors:

// Invalid: Missing required path for file response
export async function GET() {
return {
file: {
// Missing 'path' property
filename: "document.pdf",
},
};
}
// Error: File response requires 'path' property

// Invalid: Conflicting response types
export async function GET() {
return {
body: { message: "Hello" },
file: { path: "/file.pdf" },
};
}
// Error: Cannot specify both 'body' and 'file' in response

Performance Considerations

Streaming vs Buffering

// Good for large files: streams chunks
export async function GET() {
return {
stream: createReadStream("/large/file.mp4"),
};
}

// Good for small data: simple return
export async function GET() {
return { data: "small" };
}

Response Size Awareness

export async function GET() {
const data = await getPotentiallyLargeData();

if (JSON.stringify(data).length > 1024 * 1024) {
// > 1MB
// Consider streaming or pagination
return {
body: {
message: "Data too large, use paginated endpoint",
endpoint: "/api/data/paginated",
},
};
}

return data;
}

See Also