NPS.
Documentation

DevUtilityHub — Technical Reference

Architecture, API contracts, frontend structure, testing strategy, and deployment pipeline.

· 4 min read

This is the technical companion to the Functional Reference. It covers the architecture, every API contract, the frontend structure, testing approach, and the CI/CD pipeline that ships both services.


Architecture

HTTP REQUEST
Controllerroute · parse · delegate
Serviceorchestrate · validate · build response
Corepure algorithms · no HTTP · no DI
HTTP RESPONSE

The backend follows a strict three-layer pattern. Each layer has one job and no knowledge of the layers above it.

LayerLocationResponsibility
ControllerControllers/Parse the HTTP request, delegate to a service, return the HTTP response. No logic.
ServiceServices/Orchestrate Core calls, validate input, construct the response DTO. Registered as Scoped via DI.
CoreCore/Pure algorithm implementations. No HTTP context, no DI, no external state. Freely unit-testable.
ModelsModels/Request and response DTOs only. No domain logic or validation attributes.
MiddlewareMiddleware/Cross-cutting concerns. ExceptionMiddleware catches all unhandled exceptions and returns a consistent JSON error.
The Core layer has zero external dependencies. Every algorithm can be called directly in a unit test with a plain function call.

Technology Stack

Backend

ASP.NET Core Web API — .NET 10

ConcernLibrary / approach
Framework.NET 10 · ASP.NET Core Web API
LanguageC# 13 with nullable reference types and implicit usings enabled
Text diffingDiffPlex 1.9.0
Date parsingMicrosoft.Recognizers.Text.DateTime 1.8.13
API docsSwashbuckle.AspNetCore 10.1.7 + Microsoft.AspNetCore.OpenApi 10.0.5
TestingxUnit 2.9.3 · Coverlet 6.0.4
JSONSystem.Text.Json (in-box, no Newtonsoft)
XML.NET XmlDocument / XDocument
PersistenceNone — fully stateless

Frontend

React 19 · TypeScript · Vite

ConcernLibrary / version
UI frameworkReact 19.2.4
LanguageTypeScript 6.0.2 · strict mode
Build toolVite 8.0.4
RoutingReact Router 7.14.0
HTTP clientAxios 1.15.0
CSSTailwind CSS v4 · custom CSS property design system
FontJetBrains Mono (Google Fonts)
LintingESLint 9.39.4 · typescript-eslint 8.58.0

API Reference

All endpoints accept and return application/json. Every response body includes two common fields:

  • isValid: boolean — whether the operation succeeded
  • errorMessage: string | null — parse or validation error detail; null on success

A 500 response always returns { "error": "An unexpected error occurred." }. The detail field is only appended in the Development environment.

The max request body size is 1 MB. Larger payloads receive a 413.


Health check

GET/healthLiveness probe

No request body. Used by the frontend’s ApiStatusContext on mount and by Azure App Service health probes.

{ "status": "ok" }

Format / Minify / Validate

POST/api/format/processJSON · XML formatter
▸ Request
{
  "input":        "string   — text to process",
  "action":       "\"format\" | \"minify\" | \"validate\"",
  "overrideType": "\"json\" | \"xml\" | \"plain\"   (optional)"
}
▸ Response
{
  "output":       "string   — transformed text (same as input for validate)",
  "detectedType": "\"json\" | \"xml\" | \"plain\"",
  "isValid":      "boolean",
  "errorMessage": "string | null"
}

Detection order: JSON parse → XML parse → plain text fallback. overrideType skips this sequence entirely.


Text Transform

POST/api/text/transformPipeline of text operations
▸ Request
{
  "input":      "string",
  "operations": ["trim", "uppercase", "sort"]
}
▸ Response
{
  "output":            "string",
  "appliedOperations": ["trim", "uppercase", "sort"],
  "isValid":           "boolean",
  "errorMessage":      "string | null"
}

Operations run sequentially in the order provided. appliedOperations in the response reflects exactly what was executed — the UI displays this as a pipeline tag list.

Supported operation values

ValueScopeNotes
uppercasefull text
lowercasefull text
titlecasefull text
camelcasefull text
snakecasefull text
kebabcasefull text
trimper line
sortper lineCase-sensitive
dedupper linePreserves order of first occurrence
reverseper lineReverses line order, not character order
countper lineReturns a bare integer string, e.g. "42"

Diff

POST/api/diff/compareLine-level text diff
▸ Request
{
  "textA": "string   — original",
  "textB": "string   — modified"
}
▸ Response
{
  "lines": [
    {
      "type":       "\"added\" | \"removed\" | \"unchanged\"",
      "content":    "string",
      "lineNumber": 1
    }
  ],
  "addedCount":   "integer",
  "removedCount": "integer",
  "isValid":      "boolean",
  "errorMessage": "string | null"
}

Powered by DiffPlex (InlineDiffBuilder in word-diff mode, line granularity). lineNumber is 1-indexed and represents position in the diff output, not in either source text. Empty inputs return an empty lines array with counts of 0.


Timestamp Convert

POST/api/time/convertUnix ↔ human-readable
▸ Request
{
  "direction":      "\"toHuman\" | \"toUnix\"",
  "unixValue":      "integer | null   — required when direction = \"toHuman\"",
  "isMilliseconds": "boolean          — true if unixValue is in ms (default: false)",
  "humanValue":     "string  | null   — required when direction = \"toUnix\"",
  "timeZoneId":     "string  | null   — IANA timezone ID, e.g. \"America/New_York\""
}
▸ Response
{
  "seconds":      "integer",
  "ms":           "integer",
  "utc":          "string   — RFC 2822, e.g. \"Wed, 15 Nov 2023 06:13:20 GMT\"",
  "iso":          "string   — ISO 8601, e.g. \"2023-11-15T06:13:20.0000000Z\"",
  "local":        "string | null   — offset-aware, null if no timeZoneId given",
  "isValid":      "boolean",
  "errorMessage": "string | null"
}

humanValue accepts: ISO 8601, YYYY-MM-DD HH:mm, YYYY-MM-DD, MM/DD/YYYY, the shorthands now / today / yesterday / tomorrow, and natural-language expressions via Microsoft.Recognizers.Text.DateTime (e.g. "next Monday", "3 days ago").


Encode / Decode

POST/api/encode/processBase64 · URL · HTML (stub)
▸ Request (planned)
{
  "input":     "string",
  "operation": "\"encode\" | \"decode\"",
  "encoding":  "\"base64\" | \"url\" | \"html\""
}

Frontend Architecture

Pages and routes

RouteComponentAPI call
/HomePageGET /health (via ApiStatusContext)
/formatterFormatterPagePOST /api/format/process
/textTextToolsPagePOST /api/text/transform
/diffDiffPagePOST /api/diff/compare
/timeTimePagePOST /api/time/convert
/encodeEncodingPagePOST /api/encode/process

useApi<T> — shared async state hook

Every page goes through this single hook:

const { data, loading, error, call, reset } = useApi<DiffResponse>();

// Trigger a call:
await call(() => diffApi.compare({ textA, textB }));

call sets loading: true, awaits the promise, then sets either data or error. It also notifies ApiStatusContext of the outcome so the connection indicator in the header stays current.

ApiStatusContext

Global API connection state machine

StateMeaning
idleNo call has been made yet
checkingHealth probe in flight (on mount)
okLast request returned a 2xx response
network-errorRequest received no response (API unreachable)
server-errorRequest returned a 5xx response

API client modules

Each tool has a dedicated module in src/api/. All share one Axios instance configured in src/api/client.ts:

export const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  headers: { "Content-Type": "application/json" },
});

VITE_API_BASE_URL is the only environment variable the frontend needs.


Data Flow

A worked example — user formats a JSON string:

User types JSON → clicks PRETTIFY


FormatterPage.tsx
  collects { input, action: "format" }
  calls formatApi.process(request)


src/api/client.ts  [Axios]
  POST /api/format/process   →   FormatController.cs


FormatController.cs
  [HttpPost("process")]
  delegates to _formatService.Process(request)


FormatService.cs
  InputTypeDetector.Detect(input)  →  "json"
  JsonFormatter.Format(input)       →  pretty-printed string
  returns FormatResponse { output, detectedType: "json", isValid: true }


HTTP 200  { output: "...", detectedType: "json", isValid: true }


useApi sets data  →  FormatterPage renders output in the right pane

Configuration & Environment

Backend

No secrets in config files. All configuration is through environment variables set on the host or in Azure App Service application settings.

VariableWhere setEffect
ASPNETCORE_ENVIRONMENTHost / Azure config"Development" enables Swagger UI and includes exception detail in 500 responses

CORS allowed origins are hardcoded in Program.cs:

  • https://devtoolkit.nikkapaola.com (production frontend)
  • http://localhost:5173 (local Vite dev server)

Frontend

VariableRequiredWhere set
VITE_API_BASE_URLYesLocal: .env file · Production: Cloudflare Pages environment variable injected at build time

Testing

Unit tests target the Core layer directly. No HTTP context, no DI container, no mocks — just function calls.

The test project at backend/DevUtilityHub.Tests/ mirrors the Core module structure. Services are also tested where orchestration logic warrants it.

cd backend
dotnet test

Test coverage by file

FileTargetScope
FormatServiceTests.csFormatService30+ cases — JSON/XML format, minify, validate, malformed input, detection override
TextTransformServiceTests.csTextTransformServiceAll 11 operations individually and in pipeline combinations
DiffServiceTests.csDiffServiceEmpty inputs, identical texts, single-change diffs, multi-line diffs
TimestampServiceTests.csTimestampServiceUnix → human and human → Unix in both directions, timezone offsets
TimestampConverterTests.csTimestampConverterShorthands (now/today), natural language strings, IANA timezone resolution

CI/CD & Deployment

Two independent GitHub Actions workflows, each scoped to its own directory path. Neither workflow runs if only the other service changed.

Frontend → Cloudflare Pages

Workflow: .github/workflows/deploy-frontend.yml
Trigger: Push to main with changes in frontend/**, or manual dispatch.

1. Checkout
2. Node 20 setup (npm cache)
3. npm ci
4. npm run build   ← VITE_API_BASE_URL injected from GitHub secret
5. cloudflare/pages-action@v1  →  deploys dist/

Required GitHub secrets

SecretPurpose
VITE_API_BASE_URLProduction backend URL baked into the JS bundle at build time
CLOUDFLARE_API_TOKENCloudflare API access
CLOUDFLARE_ACCOUNT_IDCloudflare account identifier
CF_PAGES_PROJECT_NAMECloudflare Pages project name

Backend → Azure App Service

Workflow: .github/workflows/deploy-backend.yml
Trigger: Push to main with changes in backend/**, or manual dispatch.

Two jobs run sequentially:

Job 1: build-and-test

dotnet restore
dotnet build --configuration Release
dotnet test

Job 2: build-and-deploy (only runs if tests pass)

dotnet publish --configuration Release --runtime linux-x64 --self-contained
azure/login@v2
azure/webapps-deploy@v3  →  App Service: dev-utility-hub-api

The publish is --self-contained so no .NET runtime is required on the host. The App Service plan is Azure Free F1 — this is why Docker is not used in production; F1 does not support container deployment.

Required GitHub secrets

SecretPurpose
AZURE_CREDENTIALSService principal JSON used by azure/login@v2

Docker

Docker is available for local development only. The Dockerfile lives at backend/DevUtilityHub.Api/Dockerfile.

Dockerfile build stages

StageBase imagePurpose
basemcr.microsoft.com/dotnet/aspnet:10.0Runtime-only base; exposes ports 8080 and 8081
buildmcr.microsoft.com/dotnet/sdk:10.0Restore NuGet packages and build in Release config
publishbuilddotnet publish with UseAppHost=false
finalbaseProduction image — copies published output, sets entrypoint
debugbaseInstalls curl + vsdbg; builds in Debug; exposes port 4024 for VS Code remote debugger
# Build and run production image locally
docker build -t devutilityhub-api ./backend/DevUtilityHub.Api
docker run -p 5294:8080 -e ASPNETCORE_ENVIRONMENT=Development devutilityhub-api

A docker-compose.debug.yml at the repo root maps localhost:5294 → 8080 (API) and localhost:4024 → 4024 (VS Code remote debugger) for integrated debugging:

docker compose -f docker-compose.debug.yml up