A full-stack web application that takes a GitHub pull request URL, loads the PR metadata and diffs via the GitHub REST API, asks OpenRouter for a structured code review, lets you preview and deselect comments, then posts the selected comments back to the PR (inline review comments when GitHub accepts them; otherwise a single PR/issue comment with the full summary).
- GitHub Pull Request Reviewer
| Area | Details |
|---|---|
| Frontend | React + TypeScript + Tailwind; PR URL input; review preview checklist; loading and success/error states; activity log (server + client messages). |
| Validation | Debounced PR URL validation (https://github.com/{owner}/{repo}/pull/{number}). |
| Resilience | Client-side retries with backoff on failed review requests. |
| Backend | Express POST /api/review-pr/generate + POST /api/review-pr/post; modular githubService, openRouterService, reviewController. |
| AI output | JSON array: { file, line, comment, suggestedCode } per finding. |
| GitHub | Inline PR review comments (with suggested code when provided); always posts a PR-level summary comment. |
flowchart LR
subgraph browser [Browser]
UI[React UI]
end
subgraph vite [Vite dev server]
Proxy["/api proxy → backend"]
end
subgraph backend [Node backend]
API[Express]
RC[reviewController]
GH[githubService]
OR[openRouterService]
end
subgraph external [External APIs]
GITHUB[GitHub REST API]
OPENROUTER[OpenRouter API]
end
UI --> Proxy
Proxy --> API
API --> RC
RC --> GH
RC --> OR
GH --> GITHUB
OR --> OPENROUTER
In development, the frontend talks to the same origin (localhost:5173); Vite forwards /api/* to the Express server (default http://localhost:3001). In production, you typically serve the built static files and point the UI at your API base URL (or put both behind one reverse proxy).
| Layer | Technologies |
|---|---|
| Frontend | React 18, TypeScript, Vite 5, Tailwind CSS 3 |
| Backend | Node.js 18+ (native fetch), Express 4, ES modules |
| AI | OpenRouter Chat Completions API via native fetch (https://openrouter.ai/api/v1/chat/completions) |
| GitHub | REST API v3 (api.github.com), Bearer token, X-GitHub-Api-Version header |
pr-revewer-agent-githuh/
├── .env.example # Template for secrets (copy to backend/.env)
├── README.md # This file
├── backend/
│ ├── server.js # Express app, CORS, routes, loads backend/.env
│ ├── controllers/
│ │ └── reviewController.js
│ └── services/
│ ├── githubService.js # URL parse, PR/files fetch, comments
│ └── openRouterService.js # Prompt + structured JSON review
└── frontend/
├── vite.config.ts # Dev server + /api proxy
├── src/
│ ├── App.tsx # Main UI, retries, logs
│ ├── hooks/useDebouncedValue.ts
│ └── lib/prUrl.ts # URL validation helpers
└── …
Environment variables are read only from backend/.env (path resolved next to server.js), not from the repo root.
- Node.js 18 or newer (required for global
fetchin the backend). - npm (or compatible client) to install dependencies.
- A GitHub account and a token with rights to read the target repo and create PR/issue comments (see GitHub token permissions).
- An OpenRouter API key with access to the model you configure (default:
openai/gpt-4o-mini).
Copy the example file into the backend folder (do not commit real secrets):
cp .env.example backend/.env| Variable | Required | Description |
|---|---|---|
GITHUB_TOKEN |
Yes | GitHub personal access token (classic or fine-grained) used for all GitHub API calls. |
OPENROUTER_API_KEY |
Yes | OpenRouter API secret key. |
PORT |
No | HTTP port for the API. Default: 3001. |
CORS_ORIGIN |
No | Allowed browser origin for CORS (e.g. http://localhost:5173). If unset, the server reflects a permissive default suitable for local dev; set explicitly in production. |
OPENROUTER_MODEL |
No | OpenRouter model id. Default: openai/gpt-4o-mini. |
Example backend/.env:
GITHUB_TOKEN=ghp_xxxxxxxx
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxx
PORT=3001
CORS_ORIGIN=http://localhost:5173
OPENROUTER_MODEL=openai/gpt-4o-miniRequirements depend on repository visibility and token type.
For private repositories, use a token with the repo scope so the app can read pull requests and create comments.
For public repositories only, narrower scopes may work in some setups, but repo is the simplest choice for an MVP that must always read PRs and post comments.
Grant access to the repositories you need, with permissions such as:
- Contents: read (to resolve PR context as needed by the API)
- Pull requests: read and write (read PR + files; create review/issue comments)
Consult GitHub’s documentation on token scopes if your organization enforces restrictions.
From the repository root:
cd backend && npm install
cd ../frontend && npm install1. API server
cd backend
npm run devUses node --watch to restart on file changes. Listens on PORT (default 3001).
2. Frontend
cd frontend
npm run devOpens the Vite dev server (default http://localhost:5173). Requests to /api/* are proxied to the backend (see frontend/vite.config.ts).
cd frontend
npm run buildStatic output is in frontend/dist/. Serve that folder with any static host and ensure the browser can reach the backend (same host + reverse proxy, or set CORS_ORIGIN and configure the frontend to call your API base URL; the stock dev UI uses relative /api paths).
cd backend
npm startUse a process manager (systemd, PM2, Docker, etc.) and inject backend/.env or environment variables from your secret store.
- Open the app in the browser (dev: http://localhost:5173).
- Paste a full GitHub PR URL, for example:
https://github.com/facebook/react/pull/12345 - Click Start review and wait for AI suggestions.
- In the review preview, uncheck or Remove any comments you do not want.
- Click Post N to GitHub to publish only the selected comments.
- Inspect Activity log for server-side steps and any client retry messages.
- On success, open the PR on GitHub: you should see inline review comments and/or a single summary comment if every inline attempt failed.
The UI validates the URL shape (debounced). Retry appears after an error and re-runs generation. Cancel discards the preview without posting.
Base URL in development (via proxy): same origin as the Vite app, paths under /api.
Liveness check.
Response 200
{ "ok": true }Fetches the PR and generates AI review comments without posting to GitHub.
Headers
Content-Type: application/json
Body
{
"prUrl": "https://github.com/owner/repo/pull/42"
}Success 200
{
"ok": true,
"owner": "owner",
"repo": "repo",
"pullNumber": 42,
"prTitle": "…",
"suggestions": [
{
"file": "src/app.ts",
"line": 10,
"comment": "…",
"suggestedCode": "…"
}
],
"logs": ["[ISO8601] …", "…"]
}| Field | Meaning |
|---|---|
suggestions |
AI review items (file, line, comment, optional suggestedCode) ready for preview/selection. |
logs |
Timestamped log lines for debugging or UI display. |
Client errors 400
Missing or invalid prUrl, or invalid URL format.
{
"ok": false,
"error": "…",
"logs": []
}Server / upstream errors 4xx / 5xx
GitHub or OpenRouter failures surface with ok: false and an error message; logs may contain prior steps.
Posts selected review comments to the PR on GitHub.
Headers
Content-Type: application/json
Body
{
"prUrl": "https://github.com/owner/repo/pull/42",
"comments": [
{
"file": "src/app.ts",
"line": 10,
"comment": "…",
"suggestedCode": "…"
}
]
}Success 200
{
"ok": true,
"owner": "owner",
"repo": "repo",
"pullNumber": 42,
"prTitle": "…",
"suggestionsCount": 5,
"postedInlineCount": 3,
"fallbackPosted": false,
"summaryPosted": true,
"postedInline": [{ "file": "src/app.ts", "line": 10 }],
"inlineErrors": [{ "file": "src/app.ts", "line": 99, "message": "…" }],
"logs": ["[ISO8601] …", "…"]
}| Field | Meaning |
|---|---|
suggestionsCount |
Number of comments submitted in the request. |
postedInlineCount |
How many inline PR review comments GitHub accepted. |
fallbackPosted |
true if zero inline comments succeeded; the PR summary still carries the full review. |
summaryPosted |
true when the detailed PR-level summary comment was posted (always on success). |
postedInline |
Successfully created inline comments (file + line). |
inlineErrors |
Items GitHub rejected (wrong line, path, permissions, etc.). |
logs |
Timestamped log lines for debugging or UI display. |
Client errors 400
Missing/invalid prUrl, empty comments, or no valid comment objects.
Server / upstream errors 4xx / 5xx
GitHub failures surface with ok: false and an error message; logs may contain prior steps.
- Parse URL — Expects
github.com/{owner}/{repo}/pull/{number}(http/https allowed). - Generate (
/api/review-pr/generate) — Fetches PR + changed files; OpenRouter returns a JSON array of{ "file", "line", "comment", "suggestedCode" }(large diffs may be truncated; seeopenRouterService.js). No GitHub comments are created yet. - Preview (UI) — User unchecks or removes unwanted comments (suggested code is shown when present).
- Post (
/api/review-pr/post) — Re-fetches PR head SHA; for each selected comment, creates a pull request review comment on the PR head (side: RIGHT), including a Suggested change code block whensuggestedCodeis non-empty. Then always creates one issue comment with a detailed PR review summary.
Line numbers: GitHub expects the line to exist on the right-hand side of the diff for that commit. The model is instructed accordingly; mismatches still produce entries in inlineErrors.
- Run the backend on a reachable host; restrict
CORS_ORIGINto your real front-end origin. - Do not expose
OPENROUTER_API_KEYorGITHUB_TOKENto the browser; only the backend uses them. - Serve
frontend/distover HTTPS in production. - If the UI is not on the same host as the API, configure your build or server so API calls hit the correct base URL (the stock Vite app uses relative
/apipaths, which assume a shared reverse proxy or same-origin deployment).
| Symptom | Things to check |
|---|---|
401 / 403 from GitHub |
Token expired, missing repo / pull-request permissions, or fine-grained token not allowed on that repo. |
| Inline comments fail, summary still posted | Model line numbers not on changed lines; binary files; or GitHub rejecting path/line. See inlineErrors; a PR summary comment is still posted. |
OPENROUTER_API_KEY is not set |
backend/.env missing or wrong path; ensure you run the server from any cwd (env is loaded from backend/.env next to server.js). |
| CORS errors in browser | Set CORS_ORIGIN to your exact front-end origin (scheme + host + port). |
| Empty or invalid AI JSON | Rare model drift; retry or switch OPENROUTER_MODEL. Invalid JSON throws in openRouterService.js and returns an error response. |
| Frontend cannot reach API | Dev: confirm backend on port 3001 and Vite proxy in vite.config.ts. Prod: proxy or CORS as above. |
- Never commit
backend/.envor real tokens to git. The template is.env.exampleonly. - Never embed
GITHUB_TOKENorOPENROUTER_API_KEYin frontend code or public repos. - Rotate tokens if they leak. Prefer fine-grained tokens scoped to specific repositories when possible.
- This MVP is intended for trusted operators; it does not implement multi-user auth, rate limiting, or PR allowlists. Harden before exposing to the public internet.
| Location | Command | Purpose |
|---|---|---|
backend |
npm run dev |
Start API with node --watch. |
backend |
npm start |
Start API once (node server.js). |
frontend |
npm run dev |
Vite dev server + HMR. |
frontend |
npm run build |
Typecheck + production bundle to dist/. |
frontend |
npm run preview |
Preview the production build locally. |
Add a LICENSE file for your project if you distribute or open-source this code.