npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arkyn/server

v3.0.2

Published

Comprehensive server-side utilities for building robust backend applications, featuring HTTP response helpers, error handlers, request utilities, and API configurations.

Readme

@arkyn/server

Comprehensive server-side utilities for building robust backend applications, featuring HTTP response helpers, error handlers, request utilities, and API configurations.

npm version License TypeScript

🎯 What it solves

Backend code — Remix/React Router loaders and actions, or any fetch-based server — tends to reinvent the same plumbing on every project: consistent success/error response shapes, centralized error handling, request body/form parsing, schema validation, and validation of Brazilian documents (CPF/CNPJ/CEP/RG) plus generic fields (email/password/phone/date). @arkyn/server packages all of that into small, well-typed primitives so route handlers stay focused on business logic instead of response boilerplate.

✨ Features

  • 🌐 HTTP success responses - Success, Created, Updated, Found, NoContent with .toResponse() / .toJson()
  • 🚨 HTTP error responses - BadRequest, Unauthorized, Forbidden, NotFound, Conflict, UnprocessableEntity, ServerError, BadGateway, NotImplemented
  • 🧵 Request utilities - body decoding, scoped query params, form parsing (sync/async), error-message extraction
  • 🧪 Schema validation via Zod - SchemaValidator and the underlying formParse / formAsyncParse helpers
  • 🇧🇷 Brazilian document validators - validateCpf, validateCnpj, validateCep, validateRg
  • 🔤 Generic validators - validateEmail, validatePassword, validatePhone, validateDate
  • 🛠️ Debug & logging services - DebugService, LogService, flushDebugLogs, plus ApiService for outbound HTTP calls

📋 Prerequisites

  • Node.js >=18.0.0 or Bun >=1.0.0
  • Peer dependencies (install alongside @arkyn/server):
    • zod >=4.4.3 — required by SchemaValidator, formParse, and formAsyncParse.
    • libphonenumber-js >=1.13.7 — required by validatePhone.

📦 Installation

ESM only. This package ships as native ES modules with no CommonJS build — use import, not require().

npm install @arkyn/server zod libphonenumber-js

🚀 Quick Start

import { Success, BadRequest, errorHandler, decodeRequestBody } from "@arkyn/server";

export async function action({ request }: ActionFunctionArgs) {
  try {
    const body = await decodeRequestBody(request);

    if (!body.email) throw new BadRequest("Email is required");

    const user = await createUser(body);
    return new Success("User created", { user }).toJson();
  } catch (error) {
    return errorHandler(error);
  }
}

📖 API Reference

HTTP Responses

Every response class extends a base with .toResponse() (returns a Response with a Content-Type: application/json header) and .toJson() (built on Response.json()). Both produce an equivalent JSON body — pick whichever reads better at the call site. NoContent only exposes .toResponse(), since it always returns a null body.

BadGateway

HTTP 502 — the upstream server returned an invalid or unexpected response.

throw new BadGateway("Payment gateway unavailable");

BadRequest

HTTP 400 — the request is malformed or contains invalid data.

throw new BadRequest("Invalid request body");

Conflict

HTTP 409 — the request conflicts with the current state of the server (e.g. duplicate record).

throw new Conflict("Email already in use");

Forbidden

HTTP 403 — authenticated but not authorized to access this resource.

throw new Forbidden("You don't have permission to delete this resource");

NotFound

HTTP 404 — the requested resource does not exist.

throw new NotFound("Product not found");

NotImplemented

HTTP 501 — the server does not support the functionality required to fulfill the request.

throw new NotImplemented("Webhook delivery is not yet implemented");

ServerError

HTTP 500 — an unexpected condition prevented the server from fulfilling the request.

throw new ServerError("Failed to connect to the database");

Unauthorized

HTTP 401 — the request lacks valid authentication credentials.

throw new Unauthorized("Invalid or expired token");

UnprocessableEntity

HTTP 422 — the request is well-formed but contains semantic validation errors. Typically used for form field validation failures.

throw new UnprocessableEntity({
  message: "Validation failed",
  fieldErrors: { email: "Invalid email format", age: "Must be 18 or older" },
  fields: { email: "not-an-email", age: "15" },
});

Created

HTTP 201 — the request succeeded and a new resource was created.

return new Created("User created successfully", { id: user.id }).toJson();

Found

HTTP 302 — the resource was located and the response includes it in the body.

return new Found("Products retrieved", { products }).toJson();

NoContent

HTTP 204 — the request succeeded but there is no content to return. Typically used for delete or update operations where a body is not needed.

return new NoContent("Record deleted").toResponse();

Success

HTTP 200 — the request succeeded and the response body contains the result.

return new Success("Order fetched", { order }).toJson();

Updated

HTTP 200 — the request succeeded and the resource was updated. Semantically equivalent to Success but signals an update operation to consumers.

return new Updated("Profile updated", { user }).toJson();

Services

ApiService

HTTP client for external API calls. Wraps fetch with a base URL, default headers, an optional auth token, and per-request overrides for get, post, put, patch, and delete.

const api = new ApiService({
  baseUrl: "https://api.example.com",
  baseToken: session.token,
  enableDebug: true,
});

const { data } = await api.get("/users/me");
const { data: created } = await api.post("/orders", { body: { productId: 1 } });

DebugService

Static service that manages stack-trace configuration for debug output, allowing specific adapter/wrapper files to be skipped so logs show the actual business-logic caller. Used internally by the HTTP response classes; setIgnoreFile / clearIgnoreFiles let you tune it, and getCaller() returns { functionName, callerInfo }.

// Skip httpAdapter.ts so debug output shows the calling route instead
DebugService.setIgnoreFile("httpAdapter.ts");

LogService

Static service for log endpoint configuration. Stores a singleton configuration containing the traffic source identifier, user token, and log ingestion URL; setConfig only applies on the first call, getConfig reads it back, and resetConfig clears it.

LogService.setConfig({
  trafficSourceId: "my-app",
  userToken: session.token,
});

LogService.getConfig(); // { trafficSourceId, userToken, apiUrl }

Utilities

decodeRequestBody

Decodes a request body into a plain object, trying JSON first then URL-encoded form data. Throws BadRequest if neither format can be parsed.

export async function action({ request }: ActionFunctionArgs) {
  const body = await decodeRequestBody(request);
  // body is now a plain JS object
}

decodeRequestErrorMessage

Extracts a human-readable error message from an API response body or a Response object. Checks data.message, data.operator_erro_message, data.error, data.error.message, and response.statusText in that order, falling back to "Missing error message".

const res = await fetch("/api/orders");
const data = await res.json().catch(() => null);
const message = decodeRequestErrorMessage(data, res);

errorHandler

Converts any thrown value into a Response. Recognizes all @arkyn/server success and error response classes, native Response objects, and falls back to a 500 ServerError for anything else. Intended to be used as the catch handler of a route action or loader.

export async function action({ request }: ActionFunctionArgs) {
  try {
    const user = await findUser(id);
    if (!user) throw new NotFound("User not found");
    return new Success("User retrieved", { user }).toJson();
  } catch (error) {
    return errorHandler(error);
  }
}

flushDebugLogs

Writes colored [name] message lines to the console, but only when NODE_ENV === "development" or DEBUG_MODE === "true". No-op in production.

flushDebugLogs({
  name: "API",
  scheme: "cyan",
  debugs: ["POST /api/users", "Status: 201"],
});

formAsyncParse

Async variant of formParse — uses safeParseAsync to support Zod schemas with async refinements. Returns { success: true, data } on success or { success: false, fieldErrors, fields } on failure.

const schema = z.object({ email: z.string().email() });
const result = await formAsyncParse([{ email: "bad" }, schema]);

if (!result.success) {
  console.log(result.fieldErrors); // { email: "Invalid email" }
}

formParse

Validates form data against a Zod schema synchronously. Returns { success: true, data } on success or { success: false, fieldErrors, fields } on failure.

const schema = z.object({ name: z.string().min(1, "Required"), age: z.number().min(18) });
const result = formParse([{ name: "", age: 15 }, schema]);

if (!result.success) {
  console.log(result.fieldErrors); // { name: "Required", age: "..." }
}

getScopedParams

Extracts URL search parameters from a request, optionally filtered by a namespace prefix (e.g. scope:keykey). Without a scope, returns all search params as-is.

// URL: /products?table:page=2&table:sort=asc&other=1
const params = getScopedParams(request, "table");
params.get("page"); // "2"
params.get("sort"); // "asc"

SchemaValidator

Wraps a Zod schema with convenience validation methods suited for server-side use: isValid (boolean check, never throws), safeValidate (raw Zod result, never throws), validate (throws ServerError, for trusted/internal data), and formValidate / formAsyncValidate (throw UnprocessableEntity with structured field errors, for user-submitted forms).

const validator = new SchemaValidator(z.object({ email: z.string().email() }));

// Inside a Remix action:
const body = validator.formValidate(await decodeRequestBody(request));

Validators

validateCep

Validates a Brazilian CEP (postal code). A valid CEP must contain exactly 8 numeric digits, optionally formatted as "12345-678".

validateCep("12345-678"); // true
validateCep("12345678"); // true
validateCep("ABCDE-123"); // false

validateCnpj

Validates a Brazilian CNPJ (Cadastro Nacional da Pessoa Jurídica) number: sanitizes non-digit characters, checks length (14 digits), rejects repeating-digit sequences, and verifies both check digits.

validateCnpj("12.345.678/0001-95"); // false
validateCnpj("11.444.777/0001-61"); // true

validateCpf

Validates a Brazilian CPF number. Strips formatting, checks length, rejects repeated-digit sequences, and verifies both check digits with the CPF algorithm.

validateCpf("123.456.789-09"); // false
validateCpf("111.444.777-35"); // true

validateDate

Validates a date string against a format and optional year bounds. inputFormat accepts "brazilianDate" (DD/MM/YYYY, default), "isoDate" (MM-DD-YYYY), or "timestamp" (YYYY-MM-DD); minYear defaults to 1900 and maxYear to 3000.

validateDate("31/12/2023"); // true
validateDate("2023-12-31", { inputFormat: "timestamp", minYear: 2000, maxYear: 2100 }); // true
validateDate("29/02/2023"); // false (not a leap year)

validateEmail

Validates an email address comprehensively: basic format, advanced RFC 5322 syntax rules, and DNS resolution (MX/A/AAAA records) of the domain. Async because of the DNS lookup.

await validateEmail("[email protected]"); // true
await validateEmail("[email protected]"); // false (invalid domain)
await validateEmail("invalid-email"); // false (invalid format)

validatePassword

Validates a password: at least 8 characters, at least 1 uppercase letter, at least 1 letter, at least 1 number, and at least 1 special character.

validatePassword("Senha@123"); // true
validatePassword("senha123"); // false (no uppercase, no special char)

validatePhone

Validates an international phone number using libphonenumber-js, then confirms the parsed country code is present in the supported countries list.

validatePhone("+5532912345678"); // true (Brazil)
validatePhone("+55329123456178"); // false (invalid)

validateRg

Validates a Brazilian RG (Registro Geral) with a generic structural check: removes non-alphanumeric characters, requires a length of 7–9 characters, and optionally allows a trailing letter verifier.

validateRg("12.345.678-9"); // true
validateRg("MG-12.345.678"); // false (not supported)
validateRg("12345678X"); // true

🔧 Advanced Usage

Parse → validate → respond pipeline

import {
  decodeRequestBody,
  SchemaValidator,
  Success,
  errorHandler,
} from "@arkyn/server";
import { z } from "zod";

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

const userValidator = new SchemaValidator(createUserSchema);

export async function action({ request }: ActionFunctionArgs) {
  try {
    const body = await decodeRequestBody(request);

    // Throws UnprocessableEntity with fieldErrors/fields on failure
    const data = userValidator.formValidate(body);

    const user = await createUser(data);
    return new Success("User created", { user }).toJson();
  } catch (error) {
    return errorHandler(error);
  }
}

Brazilian document validation before persisting

import {
  decodeRequestBody,
  validateCpf,
  validateEmail,
  UnprocessableEntity,
  Created,
  errorHandler,
} from "@arkyn/server";

export async function action({ request }: ActionFunctionArgs) {
  try {
    const body = await decodeRequestBody(request);
    const fieldErrors: Record<string, string> = {};

    if (!validateCpf(body.cpf)) fieldErrors.cpf = "Invalid CPF";
    if (!(await validateEmail(body.email))) fieldErrors.email = "Invalid email";

    if (Object.keys(fieldErrors).length > 0) {
      throw new UnprocessableEntity({
        message: "Validation failed",
        fieldErrors,
        fields: body,
      });
    }

    const customer = await createCustomer(body);
    return new Created("Customer created", { customer }).toJson();
  } catch (error) {
    return errorHandler(error);
  }
}

📚 Documentation

Full documentation: https://docs.arkyn.dev/docs/server/introduction

📄 License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.