Build a TypeScript health endpoint without a framework
A first server project should be small enough to understand end to end. This one returns JSON from /health, accepts GET and HEAD, and rejects methods it does not implement. You will be able to observe a successful request, a missing route, and an invalid method without installing a web framework.
Use Node.js 24 or newer. Save the example as server.mts. The extension makes its module format explicit. Modern Node can run this erasable TypeScript syntax using built-in type stripping; Node’s TypeScript documentation explains the supported subset. Running the file this way does not type-check it.
1. Define the response contract
The Health type restricts our example payload to a single status value. It helps describe the intended shape to tooling. It does not validate incoming network data, create authentication, or prove that another service is healthy. See the TypeScript handbook for object and literal types.
import { createServer } from 'node:http';
import type { IncomingMessage, ServerResponse } from 'node:http';
const rawPort = process.env.PORT ?? '8787';
if (!/^\d+$/.test(rawPort) || Number(rawPort) < 1 || Number(rawPort) > 65535) {
throw new Error('PORT must be an integer from 1 to 65535');
}
const port = Number(rawPort);
type Health = { status: 'ok' };
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
response.setHeader('Content-Type', 'application/json; charset=utf-8');
response.setHeader('Cache-Control', 'no-store');
response.setHeader('X-Content-Type-Options', 'nosniff');
const path = (request.url ?? '/').split('?')[0];
if (path !== '/health') {
response.writeHead(404);
response.end(request.method === 'HEAD' ? undefined : JSON.stringify({ error: 'not_found' }));
return;
}
if (request.method !== 'GET' && request.method !== 'HEAD') {
response.writeHead(405, { Allow: 'GET, HEAD' });
response.end(JSON.stringify({ error: 'method_not_allowed' }));
return;
}
const payload: Health = { status: 'ok' };
response.writeHead(200);
response.end(request.method === 'HEAD' ? undefined : JSON.stringify(payload));
});
server.requestTimeout = 10_000;
server.headersTimeout = 5_000;
server.keepAliveTimeout = 5_000;
server.on('error', (error) => {
console.error(error.message);
process.exitCode = 1;
});
server.listen(port, '127.0.0.1', () => {
console.log(`Health endpoint: http://127.0.0.1:${port}/health`);
});
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
server.close(() => { process.exitCode = 0; });
server.closeIdleConnections();
const deadline = setTimeout(() => process.exit(1), 5_000);
deadline.unref();
});
}
2. Run the server locally
node server.mts
It prints an address on 127.0.0.1:8787. That binding is intentional: this exercise is reachable on the local machine instead of opening a public interface. If the port is occupied, stop this instance or choose another unused port through the PORT environment variable. The startup validation rejects malformed values and ports outside the allowed range.
3. Exercise the HTTP behavior
In another terminal, run these commands. On Windows PowerShell, use curl.exe if curl resolves to a different command.
curl -i http://127.0.0.1:8787/health
curl -I http://127.0.0.1:8787/health
curl -i http://127.0.0.1:8787/missing
curl -i -X POST http://127.0.0.1:8787/health
- GET returns HTTP 200 and
{"status":"ok"}. - HEAD returns HTTP 200 without a response body.
- A missing route returns HTTP 404.
- POST to the health route returns HTTP 405 with an Allow header.
4. Separate liveness from readiness
This endpoint shows that the process can answer a request. It does not check a database, filesystem, queue, or upstream API. Imagine a dashboard that marks a service green while its database is down: the endpoint would be technically correct about the process and misleading about the application.
A useful extension is a separate readiness route. Give every dependency check a deadline, decide whether degraded dependencies should fail readiness, and return a minimal public result. Keep diagnostic detail in restricted logs rather than putting credentials, hostnames, or stack traces into a public response.
5. Know the boundaries
The example uses HTTP on loopback and has no authentication or rate limiting. Do not expose it directly as a production application. Before deployment, decide on HTTPS termination, monitoring, process supervision, access policy, and a type-checking step with appropriate Node type definitions.
Request and header timeouts bound some connection behavior. They are not a complete defense against abusive traffic. The shutdown handler stops accepting new connections and imposes a short overall deadline. Node’s HTTP API reference describes the server interfaces used here.
Try a deliberate failure
Set PORT to a nonnumeric value and run the script. It should exit before listening. Then restore the default and test a route with a query string. Record which behavior is part of the contract and which is merely an implementation choice.
What was checked
The examples were exercised with fixture data using Python 3.14.7 and Node.js 26.7.0 on Windows. The guide targets Python 3.11+ or Node.js 24+ as specified. This is an example-project check, not a production audit or a benchmark.
Download the project files (ZIP) ↗
The ZIP contains readable source, sample log data, a README, and an MIT license. Inspect the files before running them. Nothing needs administrator access.