Skip to content

devpolas/internal_tech_issue_-_feature_tracker_api

Repository files navigation

DevPulse — Internal Tech Issue & Feature Tracker API

A collaborative platform for software teams to report bugs, suggest features, and coordinate resolutions. Built with Express 5 and TypeScript, backed by PostgreSQL with raw SQL queries, and deployed on Vercel.


Live URL

Replace with your deployed URL after deployment https://internal-tech-issue-and-feature-tra.vercel.app


Features

  • User registration and login with role-based access (contributor / maintainer)
  • JWT authentication using short-lived access tokens and long-lived refresh tokens
  • HTTP-only cookie storage for both tokens
  • Create, view, update, and delete issues (bugs and feature requests)
  • Issue workflow status management (openin_progressresolved)
  • Contributors can only update their own issues while status is open
  • Maintainers can update or delete any issue and change any status
  • Centralised global error handler with environment-aware responses (dev vs production)
  • Auto-initialised PostgreSQL schema on server startup (no migration tool needed)
  • Graceful shutdown on unhandledRejection and uncaughtException

Tech Stack

Layer Package Version
Framework Express ^5.2.1
Language TypeScript ^6.0.3
Database PostgreSQL via pg ^8.21.0
Password hashing bcrypt ^6.0.0
Auth tokens jsonwebtoken ^9.0.3
Config / env @dotenvx/dotenvx ^1.66.0
Cookies cookie-parser ^1.4.7
CORS cors ^2.8.6
Dev runner tsx (watch mode) ^4.22.3
Bundler tsup (ESM + CJS) ^8.5.1
Deployment Vercel

Project Structure

src/
├── app.ts                         # Express app: middleware, CORS, route mounting
├── server.ts                      # Entry point: DB init, server start, process safety nets
├── config/
│   └── index.ts                   # Centralised config from env vars
├── db/
│   └── index.ts                   # PostgreSQL Pool + auto schema init (CREATE TABLE IF NOT EXISTS)
├── middleware/
│   ├── auth.ts                    # authChecker (JWT guard), restrictTo (role guard)
│   └── index.d.ts                 # Express Request augmentation (req.user)
├── modules/
│   ├── auth/
│   │   ├── auth.d.ts              # UserRole, JWTSignUser, AuthTokenType types
│   │   ├── auth.controller.ts     # signup, login, refreshToken handlers
│   │   ├── auth.router.ts         # POST /signup, /login, /refresh-token
│   │   └── auth.service.ts        # DB queries, bcrypt, JWT sign/verify, cookie helpers
│   ├── issue/
│   │   ├── issue.d.ts             # IssueType, IssueStatus types
│   │   ├── issue.controller.ts    # CRUD + status update route handlers
│   │   ├── issue.router.ts        # Issue routes with auth/role middleware
│   │   └── issue.service.ts       # Raw SQL queries for all issue operations
│   └── error/
│       ├── error.ts               # AppError class (operational vs non-operational)
│       └── error.controller.ts    # Global error handler (dev: full stack, prod: safe message)
└── utils/
    ├── catch_async.ts             # Wraps async handlers — eliminates try/catch boilerplate
    └── send_response.ts           # Unified JSON response helper

Database Schema

Tables are created automatically on server startup via initDB() using CREATE TABLE IF NOT EXISTS.

users

Column Type Notes
id SERIAL PRIMARY KEY Auto-increment
name VARCHAR(255) Required
email VARCHAR(255) UNIQUE Required, unique
password VARCHAR(255) Hashed with bcrypt (12 salt rounds), never returned in responses
role VARCHAR(30) contributor (default) or maintainer
created_at TIMESTAMP Auto-set on insert
updated_at TIMESTAMP Auto-set on insert

issues

Column Type Notes
id SERIAL PRIMARY KEY Auto-increment
title VARCHAR(150) Required, max 150 chars
description TEXT Required
type VARCHAR(30) bug or feature_request
status VARCHAR(30) open (default), in_progress, resolved
reporter_id INTEGER References users(id) ON DELETE CASCADE
created_at TIMESTAMP Auto-set on insert
updated_at TIMESTAMP Auto-refreshed on update

Getting Started

Prerequisites

  • Node.js 24.x or higher
  • npm
  • A PostgreSQL database (e.g. NeonDB, Supabase, or local)

Installation

git clone <your-repo-url>
cd internal_tech_issue_and_feature_tracker_api
npm install

Environment Variables

Create a .env file in the project root:

NODE_ENV=development
PORT=5000

# PostgreSQL connection string
DATABASE_URL=postgresql://user:password@host:5432/dbname

# JWT access token (short-lived)
JWT_SECRET=your_access_token_secret_here
JWT_ACCESS_TOKEN_EXPIRE=15m

# JWT refresh token (long-lived)
JWT_REFRESH_SECRET=your_refresh_token_secret_here
JWT_REFRESH_TOKEN_EXPIRE=7d

The app uses @dotenvx/dotenvx to load .env. The schema is created automatically on first run — no migrations needed.

Scripts

# Development — hot reload via tsx watch
npm run dev

# Build for production — outputs ESM + CJS to dist/
npm run build

# Start production server
npm start

CORS

Configured to accept requests from these local origins during development:

  • http://localhost:3000
  • http://localhost:3001
  • http://localhost:5173
  • http://localhost:5174

Credentials (cookies) are enabled. Allowed methods: GET, POST, PUT, PATCH, DELETE. Update the cors config in app.ts before deploying to production.


Authentication Flow

1. POST /api/auth/signup     →  Register account (contributor or maintainer)
2. POST /api/auth/login      →  Receive access_token (body + HTTP-only cookie)
                                + refresh_token (HTTP-only cookie only)
3. Protected requests        →  Authorization: <access_token>  (header, no "Bearer" prefix)
4. Token expired?            →  POST /api/auth/refresh-token
                                (refresh_token cookie sent automatically by browser)
                             →  New access_token issued in cookie + response body

Tokens are HTTP-only cookies (secure: true in production, sameSite: lax). Passwords are hashed with bcrypt at 12 salt rounds and never returned in any response.


Roles & Permissions

Action contributor maintainer
Register / Login
View all issues
View single issue
Create issue
Update own issue (only if status is open)
Update any issue
Update issue status
Delete issue

API Reference

Base path: /api


Auth — /api/auth

POST /api/auth/signup — Register

Access: Public

Request body:

{
  "name": "John Doe",
  "email": "john.doe@devpulse.com",
  "password": "securePassword123",
  "role": "contributor"
}

Valid role values: contributor, maintainer

Response 201:

{
  "success": true,
  "message": "User Created successfully!",
  "data": {
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@devpulse.com",
    "role": "contributor",
    "created_at": "2026-01-20T09:00:00Z",
    "updated_at": "2026-01-20T09:00:00Z"
  }
}

POST /api/auth/login — Login

Access: Public

Request body:

{
  "email": "john.doe@devpulse.com",
  "password": "securePassword123"
}

Response 200: Sets access_token and refresh_token as HTTP-only cookies. Also returns token in the response body.

{
  "success": true,
  "message": "User login successfully!",
  "data": {
    "token": "<access_token>",
    "id": 1,
    "name": "John Doe",
    "email": "john.doe@devpulse.com",
    "role": "contributor"
  }
}

POST /api/auth/refresh-token — Refresh Access Token

Access: Public (reads refresh_token cookie automatically)

Response 200: Issues a new access_token cookie and returns the user payload.


Issues — /api/issues

Method Endpoint Auth Role Description
GET / No Get all issues (with reporter info)
GET /:id No Get single issue (with reporter info)
POST / Yes contributor, maintainer Create an issue
PATCH /:id Yes contributor(own issue), maintainer Update an issue
DELETE /:id Yes maintainer Delete an issue
PATCH /update-status/:id Yes maintainer Update issue status

*Contributors can only update their own issue if status is open. Maintainers can update any issue.

Auth header format:

Authorization: <access_token>

GET /api/issues — Get All Issues

Returns all issues with the reporter's info embedded as a nested object.

Response 200:

{
  "success": true,
  "message": "Issues retrived successfully",
  "data": [
    {
      "id": 45,
      "title": "Database connection timeout under load",
      "description": "Pool exhausts after 50+ concurrent queries",
      "type": "bug",
      "status": "open",
      "reporter": {
        "id": 1,
        "name": "John Doe",
        "role": "contributor"
      },
      "created_at": "2026-01-20T10:30:00Z",
      "updated_at": "2026-01-20T10:30:00Z"
    }
  ]
}

GET /api/issues/:id — Get Single Issue

Same shape as the array item above, wrapped in data: { ... }.


POST /api/issues — Create Issue

Request body:

{
  "title": "Database connection timeout under load",
  "description": "Pool exhausts after 50+ concurrent queries, causing 500 errors",
  "type": "bug"
}

Valid type values: bug, feature_request

Response 201:

{
  "success": true,
  "message": "Issue created successfully",
  "data": {
    "id": 45,
    "title": "Database connection timeout under load",
    "description": "Pool exhausts after 50+ concurrent queries, causing 500 errors",
    "type": "bug",
    "status": "open",
    "reporter_id": 1,
    "created_at": "2026-01-20T10:30:00Z",
    "updated_at": "2026-01-20T10:30:00Z"
  }
}

PATCH /api/issues/:id — Update Issue

All fields required.

Request body:

{
  "title": "Updated title",
  "description": "Updated description with steps to reproduce.",
  "type": "bug"
}

PATCH /api/issues/update-status/:id — Update Issue Status

Maintainer only.

Request body:

{
  "status": "in_progress"
}

Valid status values: open, in_progress, resolved


DELETE /api/issues/:id — Delete Issue

Maintainer only. Returns 204 with no data on success.

{
  "success": true,
  "message": "Issue deleted successfully"
}

Error Handling

All errors are processed through the global globalErrorController. The AppError class distinguishes between operational errors (known, expected) and programming errors (unexpected crashes).

Response by environment

NODE_ENV=development — full detail for debugging:

{
  "status": "fail",
  "error": {},
  "message": "Issue not found",
  "stack": "Error: Issue not found\n    at ..."
}

NODE_ENV=production — safe, minimal:

{
  "success": false,
  "status": "fail",
  "message": "Issue not found"
}

Non-operational errors in production always return a generic 500:

{
  "status": "error",
  "message": "something went very wrong!"
}

HTTP Status Codes

Code Usage
200 Successful GET, PATCH
201 Resource created (POST)
204 Successful DELETE (no body)
400 Validation errors, duplicate email, invalid input
401 Missing, expired, or invalid JWT
403 Valid token but insufficient role/permission
404 Resource not found
500 Unexpected server or database error

Deployment (Vercel)

The project includes a vercel.json that routes all traffic to dist/server.js:

{
  "version": 2,
  "builds": [{ "src": "dist/server.js", "use": "@vercel/node" }],
  "routes": [{ "src": "/(.*)", "dest": "dist/server.js" }]
}

Steps

  1. Build the project: npm run build
  2. Push to GitHub
  3. Import the repo in Vercel
  4. Set all environment variables from the .env section in the Vercel dashboard
  5. Deploy

Use NeonDB or Supabase for a hosted PostgreSQL instance compatible with Vercel's serverless environment.


Build Output

tsup bundles src/server.ts into both ESM and CJS formats:

dist/
├── server.js        # ESM build (used by Vercel)
├── server.cjs       # CJS build
├── server.js.map
└── server.cjs.map