Secure Lazy‑Loaded API Calls with Qwik City Endpoints
Learn how to keep API keys hidden, shrink client bundles, and use typed data loading in Qwik City by writing server‑only endpoint handlers and loading them with routeLoader$.
25 Dec 2025, 17:13 UTC

Problem Statement
When an application needs to call an external service, a common mistake is to perform the fetch directly in a client‑side component. That approach ships the API key or SDK to every visitor, exposing secrets and inflating the bundle. Qwik City solves this by letting you write pure server‑side endpoint handlers that stay on the server and only return serialized data to the browser.
Requirements
- Keep secrets (API keys, OAuth tokens) on the server.
- Transmit only the minimal data that the component needs.
- Use shared TypeScript types to avoid runtime shape errors.
- Allow the endpoint to be versioned and scaled independently.
- Maintain Qwik’s lazy‑loading and partial‑hydration benefits.
Minimal Design
The core of the solution is a route file that exports a request handler and a component that loads it via routeLoader$. The endpoint lives in src/routes/api/users.ts and is never bundled for the client.
Endpoint – src/routes/api/users.ts
import { json, type RequestHandler } from "@builder.io/qwik-city";
import type { User } from "~/types/user";
export const onGet: RequestHandler = async ({ env }) => {
const apiKey = env.get("EXTERNAL_API_KEY");
if (!apiKey) {
return json({ error: "Missing API key" }, { status: 500 });
}
const res = await fetch("https://api.example.com/users", {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
return json({ error: "External API error" }, { status: res.status });
}
const data: User[] = await res.json();
return json(data);
};
Key points:
onGetis executed only on the server; the file is not sent to the browser.- Secrets are read via
env.get, which is adapter‑specific. In Node it reads fromprocess.env, in Cloudflare it reads from the worker’s environment. - The handler returns a JSON response that Qwik will serialize automatically.
Component – src/components/user-list.tsx
import { component$, useSignal } from "@builder.io/qwik";
import { routeLoader$ } from "@builder.io/qwik-city";
import type { User } from "~/types/user";
const loadUsers = routeLoader$(async () => {
const res = await fetch("/api/users");
if (!res.ok) {
throw new Error("Failed to load users");
}
return res.json();
});
export default component$(() => {
const users = useSignal([]);
loadUsers().then((data) => (users.value = data));
return (
<ul>
{users.value.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
});
The component calls routeLoader$ which triggers an HTTP request to the server route. The browser never receives the handler code, only the JSON payload.
Trust & Data Boundaries
- Server‑Only Execution: The handler runs in the Node.js or Cloudflare runtime, not in the browser.
- Environment Variables: Store secrets in a platform‑specific secret store. In development, use a
.envfile; in production, use the platform’s secret management. - Data Flow: Client →
/api/users(server) → External API. The client receives only the serializedUser[]array.
Operational Checks
- Local Development – run
qwik devand open the browser. Inspect the Network tab; theAuthorizationheader should be absent from any request that originates from the browser. - Server Logs – add a masked log in the handler:
This confirms the key exists on the server but never leaks to the client.console.log("API key present:", apiKey?.slice(0, 4)); - Type Validation – ensure the
Usertype used in the component matches the shape returned by the API. A mismatch will surface at build time if you import the type from a shared module.
Failure Modes & Mitigation
- Cold Start Latency – Serverless deployments may experience a delay on the first request. Mitigate by deploying to an edge runtime that keeps warm or by periodically pinging the endpoint in a background job.
- Missing or Wrong Secret – Guard against
undefinedAPI keys and return a clear 500 error. In production, monitor for this error in logs. - External API Errors – The handler should propagate non‑2xx responses as JSON with an error field, and the component should handle the exception.
- Large Payloads – If the API returns more data than needed, add query parameters or pagination to limit the response size.
When to Redesign
- High traffic or rate limits: move the endpoint logic to a dedicated microservice or add server‑side caching (e.g., in-memory, Redis).
- Public API exposure: protect the route with authentication (JWT, API key) or a gateway that enforces rate limiting.
- Complex business logic: separate the endpoint into its own repository and deploy independently.
Comparison: Client Fetch vs. Server Endpoint
| Aspect | Client Fetch | Server Endpoint |
|---|---|---|
| Secrets Exposure | Exposed in bundle | Hidden on server |
| Bundle Size | Large (SDK, auth libs) | Minimal (JSON) |
| Type Safety | Runtime only | Compile‑time via shared types |
| Network Hops | Client → External API (1) | Client → Server → External API (2) |
| Latency | Depends on external API latency | Server adds overhead but can cache responses |
Practical Verification Checklist
- Run
qwik devand confirm theAuthorizationheader never appears in browser network traffic. - Open DevTools and watch the
/api/userscall appear in the Network tab; the request originates from the browser but the handler code runs on the server. - Deploy to a staging environment, trigger the endpoint, and review server logs for missing key or external API errors.
- Measure cold start latency by invoking the endpoint after a period of inactivity; compare with a direct client fetch to the external API.
- Verify that the component renders correctly and that the
Usertype matches the API contract.
Conclusion
Qwik City’s server‑only endpoint handlers let you keep secrets out of the browser, keep client bundles lean, and enforce type safety through shared TypeScript definitions. By leveraging routeLoader$ you can still enjoy Qwik’s lazy loading and partial hydration. Keep an eye on operational checks, guard against missing secrets, and be ready to scale or secure the endpoint if traffic grows or the API provider changes its policies.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.