Architecture Overview
This document describes how the unhide-ui codebase is structured and how its parts work together.
The big picture
The application has two sides that live in the same Next.js project:
- The UI — React pages and components that the user interacts with in the browser.
- The API — A set of server-side API routes (
/api/*) that the UI calls. These routes query the OpenSearch backend and return structured data.
Browser ──→ Next.js Pages (UI)
│
↓
Next.js API Routes ──→ OpenSearch Cluster
The UI never talks to OpenSearch directly. All data goes through the API routes.
Directory structure
src/
├── app/
│ ├── api/ # Server-side API route handlers
│ │ ├── search/ # POST /api/search
│ │ ├── categories/ # GET /api/categories
│ │ ├── details/ # GET /api/details/[documentType]/[id]
│ │ │ └── related/ # POST /api/details/related
│ │ ├── filters/ # POST /api/filters/search
│ │ ├── sources/ # POST /api/sources
│ │ ├── health/ # GET /api/health
│ │ └── utils/ # Shared server-side utilities
│ │ ├── client.ts # OpenSearch client singleton
│ │ ├── queries.ts # Query builder functions
│ │ ├── transformers.ts # Response transformer functions
│ │ ├── enrichers.ts # Data enrichment (logos, sources)
│ │ ├── helpers.ts # Helper functions
│ │ └── defaults.ts # Constants and configuration
│ │
│ └── [locale]/ # Internationalised pages (en / de)
│ ├── page.tsx # Landing page (/)
│ ├── results/
│ │ ├── page.tsx # Search results page (/results)
│ │ ├── details/ # Detail view (/results/details)
│ │ └── overview/ # Overview charts (/results/overview)
│ ├── privacy-policy/ # Privacy policy page
│ └── layout.tsx # Root layout (header, footer, providers)
│
├── components/
│ ├── app/ # Feature components
│ │ ├── HeroSection.tsx
│ │ ├── SearchSection.tsx
│ │ ├── InfoCards/
│ │ └── results/ # Search results components
│ │ ├── BackBar.tsx
│ │ ├── CategoryBar.tsx
│ │ └── ListResults/ # Result list, filters, pagination
│ └── layout/ # Structural components
│ ├── Header.tsx
│ ├── Footer.tsx
│ ├── Announcements.tsx
│ ├── Error.tsx
│ └── Spinner.tsx
│
├── contexts/ # React Context providers
│ ├── CategoryContext.tsx # Category counts and active category state
│ └── FilterDialogContext.tsx
│
├── hooks/ # Custom React hooks
│ ├── useRelatedResults.ts # Related items fetching with LRU cache
│ ├── useLocalStorage.ts # Persist state to localStorage
│ └── useDebounce.ts # Debounce a value
│
├── types/ # TypeScript type definitions
│ ├── common.ts # Entity types and shared types
│ ├── api.ts # Zod schemas for API requests
│ ├── opensearch.ts # OpenSearch query/response types
│ ├── ui.ts # UI component prop types
│ ├── related.ts # Related items types
│ └── source.ts # Source types
│
├── utils/ # Client-side utility functions
│ ├── apiCall.ts # Functions for calling the API routes
│ ├── dateFormatter.ts
│ ├── textUtils.ts
│ ├── displayFormatters.tsx
│ └── typeGuards.ts
│
├── i18n/ # Internationalisation config
│ ├── routing.ts # Locale routing settings
│ └── request.ts # Per-request locale handler
│
└── locales/ # Translation files
├── en.json
├── de.json
└── constants.ts
The eight entity types
Everything in this app revolves around eight types of research entity, each stored in its own OpenSearch index:
| Entity type | OpenSearch index | Display label |
|---|---|---|
dataset | hkg-dataset | Datasets |
document | hkg-document | Documents |
person | hkg-person | Experts |
institution | hkg-institution | Institutions |
software | hkg-software | Software |
event | hkg-event | Trainings |
instrument | hkg-instrument | Instruments |
datacatalog | hkg-datacatalog | Data Catalog |
All entity types share a common base (SchemaOrgBase in src/types/common.ts), aligned with schema.org vocabulary. Some types extend a CreativeWorkBase (datasets, documents, software, data catalogs).
How the API layer works
Every API route follows the same four-step pipeline:
Request → Validate → Query → Transform → Respond
-
Validate — The request body is parsed and validated against a Zod schema (defined in
src/types/api.ts). Invalid requests are rejected with a400error before any query runs. -
Query — A pure function in
src/app/api/utils/queries.tsbuilds the OpenSearch query object. These functions take parameters and return a plain query body — they do not execute anything themselves. -
Execute — The query is sent to OpenSearch via the client singleton in
src/app/api/utils/client.ts. The singleton ensures only one connection is created per server process. -
Transform — A pure function in
src/app/api/utils/transformers.tsconverts the raw OpenSearch response into clean, typed data. Enrichment (logos, favicons, source metadata) is added bysrc/app/api/utils/enrichers.ts.
This separation makes each step independently testable.
How data flows through the UI
Search flow
- Initial search
- The user types a query and selects a category in
SearchSection.tsx. - The browser navigates to
/results?searchText=...&category=.... - On mount,
CategoryContextfetches counts for all 8 entity types andListResultsfetches the first page of results.
- Search update
- When the user changes the search text,
CategoryContextre-fetches category counts with the new term (it watchessearchTextvia auseEffect) ListResultsre-fetches results- Filters are reset.
- Filter or page change
- Updating a filter or navigating to a new page only updates the URL params.
ListResultsre-fetches with the new params; category counts are not re-fetched.
Details flow
- The user clicks a result card and navigates to
/results/details?id=...&category=.... - The page fetches the full record from
GET /api/details/[documentType]/[id]. - The
useRelatedResultshook checks its module-level LRU cache. On a cache miss, it callsPOST /api/details/related, loading all 8 categories in parallel. - Results are cached (up to 50 entries). Pagination per category is supported.
Filter search flow
- As the user types in a filter search box,
useDebouncedelays the request until typing stops. POST /api/filters/searchis called with the document type, the filter field name, and the current search term.- The API runs an aggregation query with regex matching, scoped to the currently active filters.
- Matching filter values are returned and shown in the dropdown.
State management
The app uses three complementary mechanisms for state:
| Mechanism | Used for | Where |
|---|---|---|
| URL search params | Search text, active category, filters, pagination | All results pages |
| React Context | Category counts and the active category | CategoryContext.tsx |
| Custom hooks | Related items (with LRU cache), debounced values, localStorage | src/hooks/ |
URL params are the source of truth for everything search-related. This means search state is bookmarkable and shareable.
Internationalisation (i18n)
The app currently supports English only. But there are plans to add support for other languages in the future, starting with German. As such part of the app is already structured to support multiple locales. All pages live under a [locale] route segment:
/→ English (default locale is served without prefix)/de/→ German
Translation strings are in src/locales/en.json and src/locales/de.json. The next-intl library handles routing and provides a useTranslations() hook for components.
Locale detection is automatic based on browser settings. The default locale is en.
Key conventions
- Pure functions for queries and transformers —
queries.tsandtransformers.tscontain no side effects and are easy to unit test. - Zod for all API input validation — All request schemas are co-located in
src/types/api.ts. - TypeScript strict mode — All code is strongly typed. The
SchemaOrgResultItemunion type insrc/types/common.tsis the central data type for all entities. NEXT_PRIVATE_*vsNEXT_PUBLIC_*— Variables starting withNEXT_PRIVATE_are server-only; they are never sent to the browser. Variables starting withNEXT_PUBLIC_are available client-side.- pnpm only — The project enforces pnpm as the package manager via the
packageManagerfield inpackage.json.
Further reading
- Getting Started — How to run the project locally
- API Reference — Detailed documentation for each API endpoint
- Contributing Guide — Code style, testing, and workflow
- Component Guide — UI component structure
