Repolyzer is a GitHub profile and repository analyzer. It lets a user search a GitHub username, view profile details, inspect public repositories, sort and paginate repositories, open repository analyzer modals, and view a language breakdown based on the currently loaded repositories.
The project is split into two separate apps:
api: Express backend that proxies GitHub API requests, normalizes responses/errors, applies rate limiting, and caches GitHub responses in memory.web: React frontend built with Vite, Tailwind CSS, Redux Toolkit, and RTK Query.
The root package.json only exists to run both apps from the project root. The API and web apps keep their own package.json, lockfile, dependencies, and environment files.
- Search GitHub users by username.
- Debounced search input to avoid firing requests on every keystroke.
- Recent search dropdown stored in
localStorage. - Profile card with avatar, bio, stats, and contribution heatmap image.
- Repository grid with sorting and load-more pagination.
- Repository detail modal with analyzer-style stats, health signals, metadata, timeline, topics, and GitHub links.
- Language analyzer modal based on currently loaded repositories.
- Skeleton loading states that avoid showing stale user/repo data.
- In-memory backend cache for GitHub responses.
- Frontend RTK Query cache with append-on-load-more behavior.
- Centralized backend error handling and rate limiting.
- React 19
- Vite 7
- Tailwind CSS 4
- Redux Toolkit
- RTK Query
- React Redux
- Recharts
- Lucide React icons
- Node.js
- Express 5
- Axios
- dotenv
- cors
- express-rate-limit
- Vitest
- Supertest
- Node.js 20 or newer is recommended.
- npm
- A GitHub token is recommended for higher GitHub API rate limits.
Run these from the project root:
npm --prefix api install
npm --prefix web installCreate api/.env:
NODE_ENV=development
PORT=8080
FRONTEND_URL=http://localhost:5173
GITHUB_TOKEN=your_github_token_hereCreate web/.env:
VITE_API_BASE_URL=http://localhost:8080/apiYou can use the existing examples:
api/.env.exampleweb/.env.example
Start the API:
npm run dev:apiStart the frontend:
npm run dev:webDefault local URLs:
- Frontend:
http://localhost:5173 - API root:
http://localhost:8080 - API base:
http://localhost:8080/api
| Command | Purpose |
|---|---|
npm run dev:api |
Starts the Express API with Node watch mode. |
npm run dev:web |
Starts the Vite frontend dev server. |
npm run build:web |
Builds the frontend for production. |
npm run preview:web |
Serves the built frontend with Vite preview. |
npm run start:api |
Starts the API without watch mode. |
npm run test:api |
Runs backend route/controller and cache tests. |
- The user types a GitHub username in the search bar.
useDebouncewaits for typing to pause before committing the search.App.jsxstores the committed username, resets repo pagination, and saves the username in recent searches.- RTK Query calls the backend profile endpoint.
- The backend calls GitHub through the configured Axios client.
- The backend caches successful GitHub responses for 60 seconds.
- The frontend renders the profile section once fresh data matches the searched username.
- Repository data is fetched through RTK Query with sort and page params.
- Load More updates the page state; RTK Query merges the next page into the same repo cache entry.
- The language analyzer modal reads the currently loaded repo cache. It does not make a separate API request.
- Repository cards open a modal that derives analyzer metrics from the repo object already loaded in the list.
repolyzer/
package.json
README.md
api/
package.json
.env.example
src/
app.js
server.js
config/
env.js
github.js
controllers/
repos.controller.js
users.controller.js
__tests__/
cache.test.js
repos.test.js
users.test.js
middleware/
globalErrorHandler.js
rateLimiter.js
routes/
health.routes.js
index.js
repos.js
users.js
services/
cache.service.js
github.service.js
utils/
appError.js
catchAsync.js
web/
package.json
.env.example
index.html
vite.config.js
src/
main.jsx
App.jsx
app/
store.js
features/
github/
githubApi.js
githubSlice.js
components/
LanguageChart.jsx
RepoCard/
index.js
RepoCard.jsx
useRepoCard.jsx
RepoList/
index.js
RepoList.jsx
useRepoList.jsx
UserProfile/
index.js
UserProfile.jsx
useUserProfile.jsx
shared/
components/
AppHeader.jsx
ErrorMessage.jsx
SearchBar.jsx
SkeletonCard.jsx
SkeletonProfile.jsx
SortDropdown.jsx
hooks/
useDebounce.jsx
styles/
index.css
| File | Why it exists |
|---|---|
package.json |
Provides root-level commands to run API and web without mixing their dependencies. |
.gitignore |
Keeps one shared ignore list for the whole workspace. |
README.md |
Documents setup, architecture, API usage, and future plans. |
| File | Why it exists |
|---|---|
api/package.json |
Defines API-only scripts and backend dependencies. |
api/package-lock.json |
Locks backend dependency versions for repeatable installs. |
api/.env.example |
Documents the environment variables the API expects. |
api/src/server.js |
Creates the Express app and starts listening on env.PORT. |
api/src/app.js |
Builds the Express app, applies CORS, body parsers, routes, rate limiter, 404 handling, and global error handling. |
api/src/config/env.js |
Loads environment variables and converts PORT into a number before use. |
api/src/config/github.js |
Creates a shared Axios GitHub client with base URL, Accept header, and optional auth token. |
api/src/routes/index.js |
Combines resource routers under /api. |
api/src/routes/users.js |
Defines the user profile endpoint. |
api/src/routes/repos.js |
Defines the user repository collection endpoint. |
api/src/routes/health.routes.js |
Provides health responses for uptime checks. |
api/src/controllers/users.controller.js |
Handles HTTP request/response shape for user profile fetches. |
api/src/controllers/repos.controller.js |
Handles query params and response shape for repository list fetches. |
api/src/__tests__/users.test.js |
Verifies user route/controller behavior with the GitHub service mocked. |
api/src/__tests__/repos.test.js |
Verifies repo route/controller behavior, including query params passed to the service. |
api/src/__tests__/cache.test.js |
Unit-tests the in-memory cache service without mocks. |
api/src/services/github.service.js |
Owns GitHub API calls, path validation, params cleanup, GitHub error mapping, and cache usage. |
api/src/services/cache.service.js |
Provides the in-memory TTL cache used by GitHub services. |
api/src/middleware/rateLimiter.js |
Limits /api request bursts to protect the proxy and GitHub quota. |
api/src/middleware/globalErrorHandler.js |
Sends detailed errors in development and safe errors in production. |
api/src/utils/appError.js |
Standardizes operational errors with status code and status text. |
api/src/utils/catchAsync.js |
Forwards async controller rejections to Express error middleware. |
| File | Why it exists |
|---|---|
web/package.json |
Defines web-only scripts and frontend dependencies. |
web/package-lock.json |
Locks frontend dependency versions for repeatable installs. |
web/.env.example |
Documents the frontend API base URL variable. |
web/index.html |
Provides the Vite HTML entry and root DOM node. |
web/vite.config.js |
Configures Vite, React, and Tailwind integration. |
web/src/main.jsx |
Mounts React and provides the Redux store. |
web/src/App.jsx |
Coordinates search state, profile gating, repo sort/page state, stale-data loading guards, and layout. |
web/src/app/store.js |
Registers the GitHub RTK Query API slice and local GitHub reducer. |
web/src/features/github/githubApi.js |
Defines RTK Query endpoints for profile and repo data, including repo page merge behavior. |
web/src/features/github/githubSlice.js |
Stores recent searches and syncs them with localStorage. |
web/src/features/github/components/UserProfile/UserProfile.jsx |
Renders profile details, contribution chart, and language analyzer modal. |
web/src/features/github/components/UserProfile/useUserProfile.jsx |
Owns profile query state, stale-profile detection, modal state, scroll lock, and language aggregation. |
web/src/features/github/components/RepoList/RepoList.jsx |
Renders repository grid, empty state, skeletons, and Load More button. |
web/src/features/github/components/RepoList/useRepoList.jsx |
Owns repo query state, stale-repo detection, and pagination availability. |
web/src/features/github/components/RepoCard/RepoCard.jsx |
Renders compact repo card and repository detail modal. |
web/src/features/github/components/RepoCard/useRepoCard.jsx |
Derives repo analyzer metrics, timeline data, modal state, keyboard behavior, and scroll lock. |
web/src/features/github/components/LanguageChart.jsx |
Builds a pie chart from the primary language of currently loaded repositories. |
web/src/shared/components/AppHeader.jsx |
Provides the shared application header. |
web/src/shared/components/ErrorMessage.jsx |
Converts RTK/backend errors into user-facing messages. |
web/src/shared/components/SearchBar.jsx |
Provides controlled search input while preventing form reloads. |
web/src/shared/components/SkeletonCard.jsx |
Loading placeholder for repository cards. |
web/src/shared/components/SkeletonProfile.jsx |
Loading placeholder for profile fetches. |
web/src/shared/components/SortDropdown.jsx |
Lets users change GitHub repo sort params. |
web/src/shared/hooks/useDebounce.jsx |
Delays search commits until typing pauses. |
web/src/styles/index.css |
Tailwind import, GitHub dark theme variables, global button cursor behavior, and modal animations. |
Run backend tests from the project root:
npm run test:apiThe backend test suite uses Vitest and Supertest.
- User and repo route tests mock
github.service.js, so tests do not hit the real GitHub API. - Cache tests call
cache.service.jsdirectly because the cache is local application logic. - Repo tests verify that pagination and sort query params are passed into the service layer correctly.
Base URL in development:
http://localhost:8080
Frontend API base:
http://localhost:8080/api
Basic API root check.
Request:
GET /Response:
{
"name": "repolyzer-api",
"status": "running"
}Root-level health check for infrastructure or manual checks.
Request:
GET /healthResponse:
{
"status": "ok",
"service": "repolyzer-api",
"timestamp": "2026-01-01T00:00:00.000Z"
}API-scoped health check. Same response shape as /health.
Request:
GET /api/healthResponse:
{
"status": "ok",
"service": "repolyzer-api",
"timestamp": "2026-01-01T00:00:00.000Z"
}Fetches a GitHub user profile.
Request params:
| Param | Required | Description |
|---|---|---|
username |
Yes | GitHub username. |
Example:
GET /api/users/octocatSuccess response:
{
"status": "success",
"data": {
"user": {
"login": "octocat",
"name": "The Octocat",
"avatar_url": "https://avatars.githubusercontent.com/u/...",
"bio": "...",
"followers": 100,
"following": 10,
"public_repos": 8
}
}
}What it does:
- Validates and encodes
username. - Checks the in-memory cache using a normalized user cache key.
- Calls GitHub
GET /users/:usernameon cache miss. - Caches successful GitHub response for 60 seconds.
- Returns GitHub profile data inside
data.user.
Common errors:
| Status | Meaning |
|---|---|
400 |
Username is missing or empty. |
404 |
GitHub user was not found. |
500 |
GitHub or server failure. |
Fetches a paginated list of public repositories for a GitHub user.
Request params:
| Param | Required | Description |
|---|---|---|
username |
Yes | GitHub username. |
Query params:
| Query | Default | Description |
|---|---|---|
page |
1 |
GitHub repo page number. |
perPage |
30 |
Frontend-friendly page size alias. |
per_page |
30 |
GitHub-style page size alias. Used if perPage is not passed. |
sort |
updated |
GitHub sort value. Current UI uses stars, full_name, and updated. |
direction |
desc |
Sort direction sent to GitHub. |
type |
owner |
Repo type sent to GitHub. |
Example:
GET /api/repos/octocat/repos?page=1&perPage=30&sort=stars&type=ownerSuccess response:
{
"status": "success",
"results": 30,
"data": {
"repos": [
{
"id": 123,
"name": "hello-world",
"full_name": "octocat/hello-world",
"description": "My first repository",
"html_url": "https://github.com/octocat/hello-world",
"language": "JavaScript",
"stargazers_count": 42,
"forks_count": 5,
"watchers_count": 42,
"open_issues_count": 1,
"default_branch": "main",
"topics": ["demo"],
"license": {
"name": "MIT License"
}
}
]
}
}What it does:
- Validates and encodes
username. - Converts frontend
perPageinto GitHubper_page. - Removes empty query params before calling GitHub.
- Builds a cache key from username and all query params.
- Calls GitHub
GET /users/:username/reposon cache miss. - Caches successful GitHub response for 60 seconds.
- Returns
resultsas the number of repos in the current page.
Common errors:
| Status | Meaning |
|---|---|
400 |
Username is missing or empty. |
404 |
GitHub user or resource was not found. |
429 |
API rate limit middleware blocked the request. |
500 |
GitHub or server failure. |
The backend uses api/src/services/cache.service.js, a simple in-memory Map.
- Default TTL is 60 seconds.
- Cache entries store
expiresAt. - Expired entries are removed lazily when read.
- Failed GitHub requests are not cached.
- User profile cache key format:
github:user:<username>
- User repo cache key format:
github:user-repos:<username>:<serialized query params>
Why this exists:
- Reduces repeated GitHub API calls.
- Makes repeated searches feel fast.
- Helps avoid GitHub rate limits during development.
Limitations:
- Cache is per Node process.
- Cache is cleared when the API restarts.
- It is not shared between deployments or servers.
web/src/features/github/githubApi.js uses RTK Query.
For repository pages:
serializeQueryArgsusesusername-sortas the cache key.- Page is intentionally excluded from the cache key.
mergeappends new page data intocurrentCache.data.repos.- Page 1 resets the cache for new searches or sort changes.
forceRefetchmakes page changes fetch the next page even though the cache key is stable.
Why this exists:
- Load More feels like one growing list.
- The language modal can read currently loaded repos without making a separate request.
- Sorting starts a new repo cache because the result order changes.
githubSlice.js stores recent searches in localStorage.
- It keeps the latest 5 searches.
- Re-selecting an existing username moves it to the top.
- Recent searches survive browser refreshes.
Important states in App.jsx:
| State | Purpose |
|---|---|
searchValue |
Raw input value while the user types. |
username |
Debounced committed username that drives API calls. |
sort |
Current repo sort field. Also part of the RTK Query repo cache key. |
page |
Current repo page for Load More. |
isSearchActive |
Controls recent-search dropdown visibility. |
Important stale-data guards:
- Profile UI only renders when returned
user.loginmatches the currentusername. - Repo list only renders when repo owners match the current
username. - Skeletons show while a new username is replacing cached data.
Repository modals do not call a new endpoint. They analyze the repo object already loaded in the list.
Derived signals include:
- Activity score from
pushed_at. - Popularity score from stars, forks, and watchers.
- Completeness score from metadata such as description, language, license, homepage, topics, issues, and pull requests.
- Maintenance score from repo status and collaboration settings.
- Timeline from
created_at,pushed_at, andupdated_at. - Feature switches from GitHub flags like issues, pull requests, wiki, pages, discussions, and forking.
The language analyzer modal uses currently loaded repositories only.
- It does not call a separate backend endpoint.
- It counts each loaded repo by its
languagefield. - It updates after Load More because RTK Query appends new repos into the loaded cache.
- It is a primary-language breakdown, not a byte-level language breakdown.
For byte-level language stats in the future, the app would need to call GitHub's per-repo languages_url.
Backend:
AppErrormarks expected operational errors.catchAsyncforwards async controller errors to Express.globalErrorHandlerreturns full error details in development.- In production, operational errors keep their message, while unexpected errors return a generic message.
Frontend:
ErrorMessagemaps common backend/RTK statuses to user-friendly copy.- Profile not found is handled as a dedicated UI state in
App.jsx.
The API applies apiRateLimiter to /api.
Current settings:
- Window: 15 minutes
- Limit: 100 requests per IP per window
- Standard rate-limit headers enabled
- Legacy headers disabled
This protects the backend and reduces accidental GitHub API quota bursts.
- Keep API and web dependencies separate.
- Keep the root
package.jsonlimited to workspace convenience scripts. - Do not expose
GITHUB_TOKENin the frontend. - Put API-facing GitHub logic in backend services, not controllers.
- Put complex React component logic in colocated hooks.
- Keep the language modal based on loaded repo cache unless exact per-repo language bytes are added later.
The plan is to evolve Repolyzer into a reusable GitHub analyzer feature and later integrate it into the devloopfeed project.
Possible integration idea:
- Users on DevLoopFeed can connect or display their GitHub profile.
- Their DevLoopFeed profile can show GitHub profile stats, repositories, language breakdown, and repo analyzer insights.
- The current Repolyzer frontend experience can become a profile section or widget inside DevLoopFeed.
- The Express GitHub proxy can be reused or adapted behind DevLoopFeed's backend.
Future feature ideas:
- Byte-level language breakdown using GitHub
languages_url. - Repository README preview.
- Contribution and activity insights from GitHub events.
- Star/fork growth tracking.
- Saved favorite profiles.
- Authenticated GitHub connection flow.
- Shareable analyzer pages.
- Better production cache such as Redis.
- Tests for API services, controllers, and key frontend hooks.
Add a valid GITHUB_TOKEN in api/.env.
Check web/.env:
VITE_API_BASE_URL=http://localhost:8080/apiCheck api/.env:
FRONTEND_URL=http://localhost:5173Make sure the frontend URL exactly matches FRONTEND_URL in api/.env.
Load repositories first, or search a user whose repos have a primary language field. The chart uses loaded repo data only.