
@arkyn/server standardizes API responses around one unusual idea: both success and error responses are thrown, and a single errorHandler call converts whatever was thrown into the right Response. Once that pattern clicks, the rest of the package — outbound requests, logging, and debugging — builds on top of it consistently.Success, Created, Updated, NoContent, Found for success; BadRequest, NotFound, Conflict, UnprocessableEntity, and friends for errors) can be thrown from anywhere inside a route handler and caught once:typescript
import { errorHandler } from "@arkyn/server/errorHandler";import { Created } from "@arkyn/server/created";import { NotFound } from "@arkyn/server/notFound";import { Conflict } from "@arkyn/server/conflict";export async function action({ request }: Route.ActionArgs) {try {const input = await request.json();const existing = await findCustomerByEmail(input.email);if (existing) throw new Conflict("A customer with this email already exists");const customer = await createCustomer(input);if (!customer) throw new NotFound("Customer could not be created");throw new Created("Customer created successfully!", customer);} catch (error) {return errorHandler(error);}}
catch
block, so errorHandler is the single place that turns a result into
an HTTP Response. You don't need a separate return response.toResponse()
at every branch.errorHandler wraps them in a generic 500 ServerError, keeping the original error as cause for logging — you don't need a fallback branch of your own.BadResponse or success response class is instantiated, it automatically emits a debug console log naming the file and function that threw it — powered by DebugService's stack-trace inspection. This is free, but it has one sharp edge:throw new Created(...) inside your own adapter function
(as suggested above), the automatic log will point at the adapter file,
not your actual business logic. Register the adapter file with
DebugService.setIgnoreFile("httpAdapter.ts") once at startup, and the
log correctly attributes the throw to whatever called the adapter
instead.typescript
import { DebugService } from "@arkyn/server/debugService";// Once, near your adapter's definitionDebugService.setIgnoreFile("httpAdapter.ts");
ApiService instance per base URL instead of scattering raw fetch calls:typescript
import { ApiService } from "@arkyn/server/apiService";const api = new ApiService({baseUrl: "https://api.example.com",baseToken: env.EXTERNAL_API_TOKEN,enableDebug: process.env.NODE_ENV === "development",});const user = await api.get("/users/:id", { urlParams: { id: "42" } });
:param route placeholders (urlParams) over building the path with string interpolation yourself — beyond keeping the call readable, it's what lets LogService (below) group /users/123 and /users/456 under one aggregated /users/:userId log entry instead of treating every id as a distinct endpoint.decodeRequestErrorMessage(data, response) to normalize whatever shape it returns ({ message }, { error }, { error: { message } }, or just response.statusText) into a single string, instead of writing that fallback chain yourself each time.LogService is a one-time, app-wide singleton — configure it once near your entry point, before any request handling happens:typescript
import { LogService } from "@arkyn/server/logService";LogService.setConfig({trafficSourceId: env.ARKYN_TRAFFIC_SOURCE_ID,userToken: env.ARKYN_LOG_TOKEN,});
NODE_ENV is "development", even with a valid
config — don't spend time debugging "why don't my logs show up"
locally; deploy to a non-development environment to see them land.setConfig silently ignores every call after the first, so it's safe to import the configuration module multiple times without worrying about accidental resets — call resetConfig() explicitly if you genuinely need to reconfigure (typically only between test cases).ApiService's debug output nor anything you log manually filters sensitive fields automatically — that's @arkyn/shared's job, not @arkyn/server's. Run request/response bodies through parseSensitiveData and parseLargeFields before writing them anywhere persistent:typescript
import { parseSensitiveData } from "@arkyn/shared/parseSensitiveData";import { parseLargeFields } from "@arkyn/shared/parseLargeFields";function safeLog(label: string, body: unknown) {const masked = parseSensitiveData(JSON.stringify(body));const trimmed = parseLargeFields(masked, 500);console.log(label, trimmed);}safeLog("Incoming payload", { email: "user@example.com", password: "hunter2" });// Incoming payload {"email":"user@example.com","password":"****"}
parseSensitiveData's default key list (password, confirmPassword, creditCard) is a starting point, not a complete one — pass your own sensitiveKeys array covering anything your domain considers sensitive (tokens, national ID numbers, full card numbers) before this helper is safe to rely on for a given payload.ApiService (with enableDebug gated by environment) makes an outbound call, using :param route templating.@arkyn/server's response classes — success or failure — from wherever the outcome is decided.errorHandler catches it once and returns the right Response.DebugService's automatic caller log (with setIgnoreFile applied to any adapters) tells you exactly where each response was thrown from.LogService, after parseSensitiveData/parseLargeFields have redacted and trimmed it.UnprocessableEntity specifically carries fieldErrors back to the client — this guide covers the response/logging pattern in general, that one covers the form-validation contract in detail.SchemaValidator/formParse produce the same UnprocessableEntity shape either by throwing (formValidate) or by returning a discriminated result (formParse) — pick based on whether you're already inside a try/catch for other reasons.getScopedParams and decodeRequestBody are smaller utilities in the same package worth knowing about if you're parsing query strings or request bodies manually — see the server utilities for the full list.