Ant’s HTTP server model is intentionally minimal. You export a default object with a fetch function, and the runtime starts a server automatically — no setup code, no listen() call, no callback registration. Your handler receives a standard Request and returns a standard Response, which means everything you already know about the Web Fetch API applies here.
The basic server pattern
The smallest possible Ant server is three lines of meaningful code:
export default {
port: 8000,
fetch(req) {
const url = new URL(req.url);
return new Response('Hello from ' + url.pathname);
}
};
When Ant detects that the default export has a fetch property, it starts an HTTP listener on the configured port. Every incoming request is passed to fetch as a Request object. The Response you return becomes the HTTP response.
If you omit port, the server binds to a random available port.
Reading the request
The req parameter is a standard Request object. Use new URL(req.url) to parse the URL — do not try to split or regex the raw string.
HTTP method: "GET", "POST", "PUT", "DELETE", etc.
The full request URL string (e.g. "http://localhost:8000/users/42").
HTTP request headers as a Headers object. Use .get(name) to read a header.
export default {
port: 8000,
fetch(req) {
const url = new URL(req.url);
const userAgent = req.headers.get('User-Agent');
console.log('request:', req.method, url.pathname);
return new Response(`path=${url.pathname}, ua=${userAgent}`);
}
};
Reading the body
export default {
port: 8000,
async fetch(req) {
if (req.method === 'POST') {
const body = await req.json();
return Response.json({ received: body });
}
return new Response('send a POST', { status: 405 });
}
};
export default {
port: 8000,
async fetch(req) {
const text = await req.text();
return new Response(`you sent: ${text}`);
}
};
export default {
port: 8000,
async fetch(req) {
const form = await req.formData();
const name = form.get('name');
return new Response(`Hello, ${name}!`);
}
};
Building responses
Text response
return new Response('Hello, World!');
Status codes and headers
return new Response('not found: ' + url.pathname, { status: 404 });
return new Response(meow, {
headers: { 'X-Ant': 'meow' }
});
JSON response
Response.json() is a static helper that sets the Content-Type header for you:
return Response.json({ users: [] });
HTML response
const html = `<!doctype html><h1>Hello from Ant ${Ant.version}!</h1>`;
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
});
Error responses
export default {
port: 8000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/') return new Response('Welcome!');
return new Response('not found: ' + url.pathname, { status: 404 });
}
};
The server parameter
Your fetch function receives a second argument — the server object. Right now it exposes one method: server.stop(), which gracefully shuts down the HTTP listener.
Stops the server after the current request completes. No new connections are accepted after this is called.
let count = 0;
const MAX_REQUESTS = 10;
export default {
fetch(req, server) {
if (new URL(req.url).pathname.includes('favicon')) {
return new Response(null, { status: 404 });
}
count++;
if (count === MAX_REQUESTS) {
console.log('10 requests served, stopping');
server.stop();
}
return Response.json({
request: count,
remaining: Math.max(0, MAX_REQUESTS - count)
});
}
};
server.stop() is useful for ephemeral servers — test fixtures, one-shot tasks, or CLI tools that start a server to receive a single webhook.
Routing
Ant does not include a built-in router, but any JavaScript routing library works. The example below uses rou3, a lightweight radix-tree router:
import { createRouter, addRoute, findRoute } from '../rou3';
const router = createRouter();
addRoute(router, 'GET', '/', () => {
return new Response(`Welcome to Ant ${Ant.version}!`);
});
addRoute(router, 'POST', '/users/:id', (_req, params) => {
return new Response(`User ID: ${params.id}`);
});
addRoute(router, 'GET', '/users/:id/posts', (_req, params) => {
return new Response(`Posts for user: ${params.id}`);
});
addRoute(router, 'GET', '/api/v1/users', () => {
return Response.json({ users: [] });
});
export default {
port: 8000,
fetch(req) {
const url = new URL(req.url);
console.log('request:', req.method, url.pathname);
const result = findRoute(router, req.method, url.pathname);
if (result?.data) return result.data(req, result.params);
return new Response('not found: ' + url.pathname, { status: 404 });
}
};
Async handlers
Your fetch function can be async. Ant awaits the returned promise before sending the response.
addRoute(router, 'GET', '/status', async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
const result = await Promise.resolve('Hello');
return new Response(`server is responding with ${result}`);
});
Serving files
Use ant:fs and ant:path to read files from disk and serve them:
import { readFile } from 'ant:fs';
import { join } from 'ant:path';
addRoute(router, 'GET', '/fs/meow', async () => {
const file = await readFile(join(import.meta.dirname, 'meow.txt'));
return new Response(file || 'none');
});
See the filesystem page for details on ant:fs.