Features
API Routes
Bascik supports standard, portable API routes defined in TypeScript or JavaScript files under src/api/.
// src/api/contact.ts
export const POST = async (request: Request): Promise<Response> => {
const { name, email } = await request.json();
if (!email) {
return Response.json({ error: "email is required" }, { status: 400 });
}
await sendEmail({ name, email });
return Response.json({ ok: true }, { status: 201 });
}; Handlers take a standard WHATWG Request and return a standard WHATWG Response. There is no proprietary context wrapper, no middleware chain, and no custom decorator syntax. Server scripts and stream scripts receive this same Request object; see Server Scripts.
File-Based Routing
API route files live in directory.api (default: src/api). The URL path prefix is always /api:
| File Path | URL Route |
|---|---|
src/api/health.ts | /api/health |
src/api/contact.ts | /api/contact |
src/api/users/index.ts | /api/users |
src/api/users/[id].ts | /api/users/:id |
src/api/[org]/repos/[id].ts | /api/:org/repos/:id |
Static segments take precedence over dynamic segments. For example, src/api/users/me.ts is matched before src/api/users/[id].ts when navigating to /api/users/me.
When configuring a custom base in bascik.config.ts, API routes compose cleanly (e.g. base: '/app/' routes to /app/api/...).
Method Exports and Automatic Allow Headers
A route file exports functions corresponding to standard HTTP methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.
- Allowlist dispatch: Only exported methods are accepted. Any unexported method returns
405 Method Not Allowedwith an accurateAllowheader listing the permitted methods. - Derived HEAD: When
GETis exported butHEADis not, Bascik executesGET, returns the status and headers, and automatically strips the body. An explicitHEADexport takes precedence. - Auto OPTIONS: When
OPTIONSis not exported, Bascik automatically responds with204 No Contentand theAllowheader. Bascik does not inject CORS headers by default. Cross-origin access remains entirely under the handler's control.
The Request and Response Contract & Portability
Bascik uses native web standard Request and Response objects, and request.url reflects the real scheme and host of the incoming request. That makes the handler function reusable: a handler that only touches Web APIs (Request, Response, URL, fetch, Web Crypto) is plain code you can call from a unit test or from another runtime's wrapper.
Reusable code is not the same as a deployable service. Three more things have to exist on any host before a handler answers traffic: the route table (src/api/users/[id].ts to /api/users/:id), the method dispatch and Allow semantics described above, and whatever the handler imports. A handler that reads process.env, opens a database socket, or imports a Node-only package runs where those things exist.
Where handlers run today:
bascik --serverand the dev server: supported, in-process, with routing, dispatch, body limits, and timeouts handled for you.- Serverless (static assets on a CDN plus managed functions): supported through a build target and adapter, starting with Cloudflare Pages and Workers. The same routing and dispatch core runs inside the generated function. See Deployment for the support matrix, or test an endpoint live on our interactive edge API demo.
- Other hosts: manual porting only. Write the wrapper for the platform's request shape and reuse the handler function; Bascik does not generate one for you.
The Context Argument
Handlers accept an optional second argument providing parsed route parameters and client IP:
export const GET = async (
request: Request,
context: { params: Record<string, string>; remoteIp: string }
): Promise<Response> => {
return Response.json({
id: context.params.id,
ip: context.remoteIp,
});
}; context.params: Key-value map of extracted dynamic route segments ([param]).context.remoteIp: The client IP address. Whenhttp.trustProxyis enabled in configuration, this value reflects the real client IP forwarded by upstream reverse proxies or CDNs.
Request Body Handling and Streaming
Request bodies are exposed as standard WHATWG streams with duplex: 'half'. Handlers parse bodies using standard WHATWG methods such as await request.json(), await request.text(), await request.formData(), or await request.arrayBuffer(). Bascik does not automatically parse bodies or sniff content types.
- Streaming Size Enforcement (
http.maxBodySize): The maximum request body size defaults to1048576bytes (1 MB) and can be customized inbascik.config.ts. - Stream Counting: Bytes are counted on the fly as they stream from the client. Bascik never buffers an unbounded payload in memory to measure it.
- Payload Rejection: If the payload exceeds the limit, the incoming stream is aborted and destroyed, and Bascik responds with
413 Payload Too Largewithout continuing execution. - Client Headers Untrusted: Bascik does not trust
Content-Lengthheaders from clients; size limits are enforced strictly against actual received bytes.
Timeouts and Cooperative Cancellation
API route execution is protected by http.apiTimeout (defaults to 10000 ms):
- AbortSignal: Handlers receive a cooperative
AbortSignalin the third argument options object{ signal }to cancel downstream network calls or database operations. - Hard Timeout: If a handler does not resolve before
http.apiTimeoutelapses, Bascik responds with504 Gateway Timeoutand logs the timeout on the server. - Synchronous Execution Limitation: Like any JavaScript runtime, synchronous CPU-bound operations (such as infinite loops) cannot be interrupted by timers. The timeout protects asynchronous operations (promises, queries, fetch requests).
Error Handling and Information Protection
Bascik strictly protects internal implementation details from clients:
- Clean 500 Responses: Thrown errors or unhandled rejections return a generic
500 Internal Server Errorresponse with no stack traces, file paths, or internal error messages sent to the client. - Server-Side Logging: The complete, formatted error stack trace and route path are logged directly to server stderr for diagnostics.
- Fault Containment: An error or syntax failure in a specific route file is completely contained to that route. Other API routes, server scripts, and static pages continue operating normally.
- Network Resets: Client disconnects and network resets (
ECONNRESET,EPIPE,ERR_HTTP2_STREAM_CANCEL) mid-request are handled cleanly without logging false server errors.
Security Model and Secret Protection
- No CORS by Default: Same-origin policy is preserved by default. Handlers must explicitly set
Access-Control-Allow-Originheaders if cross-origin access is intended. - Full Environment Access: Handlers run in-process with full access to
process.envfor database credentials, private API tokens, and server secrets. - Source Protection: Source code in
directory.api(src/api/) is never served to clients, copied by asset pipelines, or bundled into static build output. - Transport and Traversal Security: Path traversal attempts (
%2e%2e%2f), dot-file requests, and null bytes (%00) are blocked before routing. CRLF injection in handler-supplied headers is strictly rejected.
Errors and Status Codes
- Return any standard
Responseobject to specify status code, headers, and body. - When throwing an uncaught error or returning a non-Response value, Bascik logs the error on the server and responds to the client with
500 Internal Server Error. - Multiple
Set-Cookieheaders are preserved without flattening.
Testing Handlers Without a Server
Because handlers take a Request and return a Response, you can test them directly in unit tests without starting a network server:
import { describe, it, expect } from "vitest";
import { POST } from "./contact.ts";
describe("contact API route", () => {
it("rejects requests missing an email", async () => {
const request = new Request("http://localhost/api/contact", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: "Alice" }),
});
const response = await POST(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("email is required");
});
}); Static Builds vs Production Server
Static builds (bascik --build) cannot serve dynamic API endpoints. If route files exist in src/api/, the build prints a warning and completes:
warning: 3 API routes found in src/api/ but static builds cannot serve them.
Deploy with `bascik --server`, or port them to your host's function runtime.
Routes: /api/health, /api/contact, /api/users/[id] To serve API routes, run Bascik in production server mode using bascik --server, during development using bascik, or build for a serverless target that packages the routes into a managed function (see Deployment).
What Bascik Deliberately Omits
Bascik leaves standard web application responsibilities to standard TypeScript and web APIs:
- Middleware: Compose plain functions directly inside your handler files.
- Body validation: Use Zod, Valibot, or native checks directly on
await request.json(). - CORS headers: Return explicit
Access-Control-Allow-Originheaders on responses if required. - Sessions & Auth: Read the
cookieheader and returnSet-Cookieheaders using standard Web APIs. - Automatic compression: API routes are not compressed automatically to prevent BREACH attack vectors on sensitive dynamic payloads.
Next: Learn about the production server or explore configuration options.