🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 10 Min Lesezeit
0

Migrating Ekehi from Vanilla JS to a TypeScript Stack

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Ekehi platform has moved from hand-written HTML/CSS/JS pages to a typed, component-driven React 19 client and a module-based TypeScript Express/Node.js API.






0. Where we started and where we landed



Before. A static client built from per-page folders (landing/,

contributors/, login/, signup/, admin/), each shipping its own

index.html, a shared styles.css, and vanilla ES module scripts under

client/shared/. The server was an Express API written in plain JavaScript.



After.











































Layer Before After
Client Static HTML + CSS + vanilla JS React 19 + Vite 8 + TanStack Router + TypeScript 6
Styling One global styles.css
Tailwind CSS 4 with @theme design tokens
Data
fetch scattered per page
TanStack Query over a typed lib/api client
Server Express in JavaScript Express + TypeScript, module-per-domain
Repo Two loose folders pnpm workspace with shared git hooks
Quality gate None ESLint 9, Prettier 3, Husky, commitlint, Vitest


The migration ran in two phases on separate branches:




  • **Phase 1 — client rewrite.

  • **Phase 2 — server rewrite.







1. Why this framework?



Choice: React 19, rendered as a client-side SPA through Vite 8, routed by TanStack Router.



TanStack Router was chosen rather than React Router because it

gives fully type-safe routes, first-class search-param typing, built-in code-splitting, and file-based route generation that pairs cleanly with Vite.







2. The folder and component structure



The client uses a feature-sliced layout: code is grouped by domain, not by

technical type.




CODE
client/src/
├── components/
│ ├── layout/ navbar.tsx, footer.tsx
│ └── ui/ button, input, modal, select, dropdown, ... (design system)
├── config/ env.ts, env-schema.ts, endpoints.ts
├── features/ one folder per domain
│ ├── auth/ auth.query.ts, auth.service.ts, auth.types.ts, components/, pages/
│ ├── opportunities/ pages/
│ ├── resources/ pages/
│ ├── submissions/ pages/
│ ├── admin/ pages/
│ └── site/ pages/ (landing, contributors)
├── lib/
│ ├── api/ request.ts, errors.ts, refresh.ts, types.ts (HTTP client)
│ ├── auth/ token-store.ts
│ ├── query-client.ts
│ └── utils.ts
├── routes/ TanStack file-based route tree
├── router.tsx
├── routeTree.gen.ts generated, do not edit
└── styles.css Tailwind import + @theme tokens






The principle: a route file in routes/ is thin glue that points at a page component in the matching features/<domain>/pages/ folder. Domain logic (queries, services, types) lives next to the feature that owns it, so a single Claude or dev session can work one feature without touching another.



Imports use the #/* alias (defined in both package.json#imports and tsconfig.json#paths) so there are no ../../../ chains:




CODE
import { env } from '#/config/env'
import { getAccessToken } from '#/lib/auth/token-store'









3. Decompose the UI into reusable components



The old per-page markup was factored into a small design-system layer under components/ui/: button, input, password-input, textarea, select, checkbox, label, form-field, dropdown, modal, search-bar, skeleton.



Technique and libraries:





  • Radix UI primitives (@radix-ui/react-dialog, react-dropdown-menu, react-slot) supply accessible, unstyled behaviour (focus traps, keyboard nav, ARIA). The team styles them rather than reimplementing accessibility.


  • class-variance-authority (CVA) defines component variants (size, intent) as typed config instead of ad-hoc conditional class strings.


  • clsx + tailwind-merge (wrapped in lib/utils.ts) merge class names and resolve Tailwind conflicts deterministically.


  • lucide-react provides the icon set as tree-shakeable components.



Layout components (navbar, footer) live separately under

components/layout/ because they are app chrome, not reusable primitives.







4. Migrate static HTML sections into JSX



Each former page folder became a route + page component pair. The old

landing/index.html is now features/site/pages/landing-page.tsx mounted at

routes/(layout)/index.tsx; contributors/, login/, signup/, the admin screens, and the resource/opportunity pages followed the same move.



Shared chrome that used to be copy-pasted into every index.html now lives once:





  • routes/__root.tsx is the application shell.


  • routes/(layout)/route.tsx wraps every public page in the shared navbar/footer layout, so markup is defined a single time and inherited.



The vanilla client folders (client/shared/, client/landing/,

client/contributors/, etc.) and the root index.html/styles.css were deleted in the same release (see CHANGELOG.md [2.0.0] → Removed).







5. Migrate CSS with the framework's recommended approach



Styling moved to Tailwind CSS 4, installed as a Vite plugin

(@tailwindcss/vite) rather than a PostCSS pipeline — the v4 recommended path.



The single global stylesheet was replaced by a token-driven theme declared

with the new @theme directive in client/src/styles.css:




CODE
@import 'tailwindcss';

@theme {
--color-purple-700: #730099;
--color-primary: var(--color-purple-700);
--color-surface: #ffffff;
--color-surface-subtle: var(--color-neutral-50);
/* ... */
}






Two deliberate techniques:





  • Role-based token names. Raw scales (purple-700, neutral-50) are mapped to semantic roles (primary, surface, content, line) so components reference intent, not hex values. See refactor(client): rename color tokens to role-based names (surface/content/line).


  • Deterministic class ordering. prettier-plugin-tailwindcss sorts utility classes on every format pass, so class lists never drift between authors.



Fonts are self-hosted via @fontsource-variable/inter and

@fontsource-variable/lora, and fontaine generates metric-matched fallback @font-face rules to eliminate layout shift on font swap.







6. Replace vanilla JS logic with framework equivalents



The biggest behavioural shift. Imperative fetch + DOM updates became declarative server-state managed by TanStack Query on top of a typed HTTP client.



The HTTP client (lib/api/request.ts). A makeRequest factory builds typed callers and centralises everything that used to be repeated per page:




  • attaches the bearer token from lib/auth/token-store,

  • serialises GET params vs JSON/FormData bodies,

  • unwraps the server's { success, data, meta } envelope,

  • and — critically — on a 401 it transparently calls refreshSession() and retries the original request once (skipping /auth/* routes to avoid loops):



CODE
if (response.status === 401 && !isRetry && !route.startsWith(AUTH_PATH_PREFIX)) {
const refreshed = await refreshSession()
if (refreshed) return executeRequest(route, method, options, true)
}





Server state via TanStack Query. Features expose typed hooks instead of inline fetch calls. Auth (features/auth/auth.query.ts) is representative:




CODE
export function useLoginMutation() {
const queryClient = useQueryClient()
return useMutation<LoginResponse, Error, LoginRequest>({
mutationFn: (data) => AuthService.login({ data }).then((r) => r.data),
onSuccess: (response) => {
setTokens({ access_token: response.access_token,
refresh_token: response.refresh_token })
queryClient.invalidateQueries({ queryKey: authKeys.me() })
},
})
}






Query keys are namespaced (authKeys), caching/invalidation is handled by the library, and useLogoutMutation clears the cache on settle. The shared client config lives in lib/query-client.ts.



Forms use React 19's useActionState. Login and signup wire the form action straight to the mutation, getting pending/error state from the framework with no manual loading flags.



Validation is shared end to end with Zod 4 — the same library validates client env, form input, and server request bodies.









7. Set up client-side routing



Routing is file-based through TanStack Router with autoCodeSplitting enabled in vite.config.ts, so every route is its own lazy chunk. The route tree under client/src/routes/ uses TanStack's grouping conventions:




CODE
routes/
├── __root.tsx app shell
├── (auth)/login.tsx, signup.tsx auth pages (no app chrome)
├── (layout)/
│ ├── route.tsx shared navbar/footer layout
│ ├── index.tsx landing
│ ├── opportunities/index.tsx, $id.tsx
│ ├── resources/.../$id.tsx training + guides, list + detail
│ └── (protected)/
│ ├── route.tsx auth guard
│ ├── submit.tsx, submissions.tsx, my-submissions.tsx
└── admin/index.tsx, queue.tsx, review.tsx






Three conventions do the heavy lifting:





  • (group) folders organise routes without adding URL segments — (auth), (layout), (protected) are structural, not path segments.


  • Pathless layout routes (route.tsx) inject shared UI and guards. The (protected)/route.tsx boundary enforces auth before any submission/admin page renders.


  • $id.tsx dynamic segments handle detail pages with type-safe params.



The generated routeTree.gen.ts is produced by the router plugin (or pnpm generate-routes) and must not be hand-edited.









8. Push to GitHub with clear, meaningful commits



The repo enforces Conventional Commits repo-wide through git hooks installed at the workspace root:





  • Husky 9 activates the hooks (.husky/).


  • commit-msg runs commitlint (@commitlint/config-conventional) on every message — feat(client): ..., refactor(server): ..., chore: ....


  • pre-commit runs lint-staged, auto-fixing staged files with eslint --fix and prettier --write so nothing unformatted lands.



Branching model (from CONTRIBUTING.md): branches start from and target development; main advances only via release merge. The migration itself









Server migration — JavaScript Express to TypeScript modules



Phase 2 rewrote the API in TypeScript 6 without changing the

runtime contract the client consumes.



Architecture: one module per domain. Each domain owns four files with a single responsibility each:




CODE
server/src/modules/<domain>/
├──
<domain>.routes.ts route table
├── <domain>.controller.ts HTTP in/out only
├──
<domain>.service.ts business logic + Supabase calls
└── <domain>.schema.ts Zod request validation






Domains: auth, admin, opportunities, trainings, guides, templates, profile, meta, health. The composition root is app.tsroutes.ts → each module's routes. Cross-cutting concerns live in middleware/

(authenticate, require-role, rate-limit, validate-request, upload, error-handler) and shared helpers in lib/ (response, http-error, async-handler, supabase, validation, storage, logger).



Toolchain:





  • tsx runs the dev server with watch mode (tsx watch src/server.ts) — no precompile step in development.


  • tsc + tsc-alias produce the production build, rewriting the #/* path alias to real relative paths in the emitted dist/.


  • Typed database access. pnpm db:types generates src/types/database.ts from the live Supabase schema, so queries are checked against the real table shapes.


  • Zod validates every request body before it reaches a controller, mirroring the client's use of the same library.



Security and ops middleware carried over and are now typed: helmet, cors, morgan, express-rate-limit, and multer for uploads.









Library reference



Every library introduced by the migration and the job it does.






Client
















































































Library Role

react, react-dom 19
UI runtime; useActionState for form submission

vite 8 + @vitejs/plugin-react
Build tool and dev server, React fast refresh

@tanstack/react-router (+ router-plugin)
Type-safe, file-based, code-split routing
@tanstack/react-query Server-state cache, mutations, invalidation

tailwindcss 4 + @tailwindcss/vite
Utility CSS with @theme design tokens
@tailwindcss/typography Prose styling for long-form content

@radix-ui/react-dialog, -dropdown-menu, -slot
Accessible unstyled UI primitives
class-variance-authority Typed component variants

clsx + tailwind-merge
Class composition and conflict resolution
lucide-react Icon set

zod 4
Env, form, and shared validation

@fontsource-variable/inter, -lora
Self-hosted variable fonts
fontaine Metric-matched font fallbacks (zero CLS)

typescript 6
Static typing

vitest + @testing-library/react + jsdom
Unit/component tests

eslint 9 (@tanstack/eslint-config, simple-import-sort, unicorn, unused-imports)
Linting

prettier 3 + prettier-plugin-tailwindcss
Formatting + class sorting





Server




























































Library Role

express 4
HTTP framework

typescript 6
Static typing
tsx Dev runtime with watch
tsc-alias Rewrites path aliases in the build output
@supabase/supabase-js Database + auth client (service role)

zod 4
Request validation
helmet Security headers
cors Cross-origin policy
morgan Request logging
express-rate-limit Rate limiting
multer Multipart upload handling
dotenv Env loading





Workspace




























Library Role

pnpm workspace
Single install for client + server, shared hooks

husky 9
Git hook activation

@commitlint/cli + config-conventional
Conventional Commit enforcement
lint-staged Auto-fix staged files pre-commit








What the migration bought, measurably





  • Type safety end to endpnpm typecheck (client and server) and generated Supabase types catch contract drift before runtime.


  • One quality gatepnpm check runs lint + typecheck + tests + format on both packages; pre-commit blocks unformatted code; commit-msg blocks malformed history.


  • Smaller, lazier bundlesautoCodeSplitting ships one chunk per route instead of one monolithic script.


  • Zero cumulative layout shift on first paint via fontaine fallbacks.


  • Reusable UI — a single design-system layer replaces per-page markup, so a button change lands everywhere at once.






Related docs





  • CHANGELOG.md[2.0.0] frontend rewrite entry with full Added/Changed/Removed.


  • docs/pnpm-workspace-migration.md — the workspace consolidation.


  • client/docs/tooling-plan.md — client tooling design notes.


  • client/docs/migration-shared-components.md — component extraction notes.


  • server/docs/system-design-case-study.md — server architecture rationale.


  • server/docs/api/endpoints.md — API reference the client consumes.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Migrating Ekehi from Vanilla JS to a TypeScript Stack

Thematisch verwandte Begriffe: Migrating, Ekehi, from, Vanilla · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...