Build REST APIs with file-based routing. Create .ts files in site/api/ to define API endpoints.
Basic API Route
Create site/api/(users).ts:
import type { IncomingMessage, ServerResponse } from 'node:http';
export async function handle(req: IncomingMessage, res: ServerResponse) {
if (req.method === 'GET') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ users: [] }));
} else {
res.writeHead(405);
res.end('Method Not Allowed');
}
}
Access at GET /api/users
Request Handling
export async function handle(req, res) {
console.log(req.method); // GET, POST, etc.
console.log(req.url); // /api/users?page=1
console.log(req.headers); // Headers object
// Route methods
if (req.method === 'GET') {
// Handle GET
} else if (req.method === 'POST') {
// Handle POST
} else if (req.method === 'PUT') {
// Handle PUT
} else if (req.method === 'DELETE') {
// Handle DELETE
}
}
Reading Request Body
export async function handle(req, res) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const data = JSON.parse(body);
res.writeHead(200);
res.end(JSON.stringify({ success: true, data }));
} catch (err) {
res.writeHead(400);
res.end('Invalid JSON');
}
});
}
Query Parameters
export async function handle(req, res) {
const url = new URL(req.url || '', `http://${req.headers.host}`);
const page = url.searchParams.get('page') || '1';
const limit = url.searchParams.get('limit') || '10';
const users = await getUsers(parseInt(page), parseInt(limit));
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(users));
}
Response Headers
export async function handle(req, res) {
res.writeHead(200, {
'content-type': 'application/json',
'cache-control': 'public, max-age=3600',
'x-custom-header': 'value'
});
res.end(JSON.stringify({ data: [] }));
}
Status Codes
// Success
res.writeHead(200); // OK
res.writeHead(201); // Created
res.writeHead(204); // No Content
// Errors
res.writeHead(400); // Bad Request
res.writeHead(401); // Unauthorized
res.writeHead(403); // Forbidden
res.writeHead(404); // Not Found
res.writeHead(500); // Server Error
JSON Responses
function sendJSON(res, data, status = 200) {
res.writeHead(status, { 'content-type': 'application/json' });
res.end(JSON.stringify(data));
}
export async function handle(req, res) {
if (req.method === 'GET') {
sendJSON(res, { users: [] });
} else {
sendJSON(res, { error: 'Method not allowed' }, 405);
}
}
Dynamic Routes
Create nested API routes with dynamic parameters:
// site/api/users/($id).ts
export async function handle(req, res) {
const url = new URL(req.url || '', `http://${req.headers.host}`);
const userId = url.pathname.split('/')[3]; // Extract from path
if (req.method === 'GET') {
const user = await getUser(userId);
if (user) {
sendJSON(res, user);
} else {
sendJSON(res, { error: 'Not found' }, 404);
}
}
}
Access at GET /api/users/123
Error Handling
export async function handle(req, res) {
try {
const data = await fetchData();
sendJSON(res, data);
} catch (err) {
console.error(err);
sendJSON(res, { error: 'Server error' }, 500);
}
}
CORS
Handle cross-origin requests:
function setCORSHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
export async function handle(req, res) {
setCORSHeaders(res);
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
// Handle request
}
Authentication
import { verifyToken } from '@src/auth/jwt';
export async function handle(req, res) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
sendJSON(res, { error: 'Unauthorized' }, 401);
return;
}
try {
const payload = verifyToken(token);
// Use payload.userId, etc.
} catch (err) {
sendJSON(res, { error: 'Invalid token' }, 401);
}
}
Validation
function validateInput(data) {
if (!data.email || !data.name) {
return { valid: false, error: 'Missing fields' };
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
return { valid: false, error: 'Invalid email' };
}
return { valid: true };
}
export async function handle(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
const data = JSON.parse(body);
const validation = validateInput(data);
if (!validation.valid) {
sendJSON(res, { error: validation.error }, 400);
return;
}
// Process valid data
});
}
File Uploads
export async function handle(req, res) {
if (req.method === 'POST') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
// Parse multipart/form-data or base64
const file = parseUpload(body);
// Save file
await saveFile(file);
sendJSON(res, { success: true });
});
}
}
Streaming Response
export async function handle(req, res) {
res.writeHead(200, { 'content-type': 'application/json' });
res.write('{"items":[');
for (let i = 0; i < 1000; i++) {
res.write(JSON.stringify({ id: i }));
if (i < 999) res.write(',');
}
res.write(']}');
res.end();
}
Best Practices
- Validate all inputs
- Use appropriate HTTP methods
- Return meaningful status codes
- Implement authentication
- Handle errors gracefully
- Use proper error responses
- Document your API
- Version your API endpoints