Skip to content

Repository files navigation

InternTrack API

A backend-only REST API, built with Spring Boot, for tracking internship applications end to end — from logging a new application through to interview reminders, CV storage, and AI-powered gap analysis against job descriptions.

Every user's data is fully isolated (JWT-based auth, ownership checks on every request), interview reminders go out automatically by email, uploaded CVs are stored persistently via Cloudinary, and each application can be analyzed against its own job description using Gemini — comparing the applicant's actual CV, not a fixed skill list. Built as a hands-on exercise in layered Spring Boot architecture: Controller → Service → Repository, with caching, scheduling, file storage, and an external AI integration each handled as their own concern.

Architecture

                         ┌──────────────────┐
                         │   JWT Filter     │
                         │ (auth on every   │
                         │    request)      │
                         └────────┬─────────┘
                                  │
 Client ───HTTP───▶  Controller ─┼─▶ Service ───▶ Repository ───▶ PostgreSQL
 (Swagger/           (REST API)   │  (business    (Spring Data     (Neon)
  Postman)                        │   logic)       JPA)
                                  │         │
                        ┌─────────┴──┐   ┌──┴───────────────┐
                        │   Cache    │   │  GeminiService   │
                        │ (dashboard │   │ (AI gap analysis │
                        │  stats,    │   │  via Gemini API) │
                        │  per-user  │   └──────────────────┘
                        │    key)    │
                        └────────────┘

                        ┌────────────────────┐
                        │     Scheduler      │
                        │ (runs every 5 min, │
                        │  independent of    │
                        │  HTTP requests)    │
                        └─────────┬──────────┘
                                  │
                                  ▼
                        Repository ──▶ PostgreSQL
                                  │
                                  ▼
                            EmailService ──▶ Gmail SMTP

Every incoming request passes through the JWT Filter before reaching a controller, except for /api/auth/register and /api/auth/login. The Scheduler runs independently of any client request, on its own timer, and talks directly to the repository and mail layer. AI requests flow through their own dedicated GeminiService, kept separate from the rest of the business logic in ApplicationService.

Live Demo

The API is deployed and publicly accessible:

Note: The free Render instance spins down after 15 minutes of inactivity. The first request after idling may take a few minutes (up to ~3 minutes) to respond while it wakes up.

Tech Stack

  • Java 17
  • Spring Boot 3
  • Spring Data JPA
  • Spring Security + JWT
  • PostgreSQL
  • Redis (caching, via Spring Cache abstraction — Simple Cache locally, Redis in Codespaces)
  • Spring Mail + Spring Scheduler (automated interview reminder emails)
  • Cloudinary (persistent storage for uploaded CV files)
  • Google Gemini API (AI-powered job fit / gap analysis)
  • JUnit 5 + Mockito (unit tests for core business logic)
  • Docker (containerized deployment on Render)
  • Swagger / OpenAPI
  • Lombok

Deployment Architecture

The application is deployed as a Docker container on Render, connected to a Neon PostgreSQL database (both free tier). Every push to main triggers the GitHub Actions test suite; Render then auto-deploys from main via its own build hook.

How to Run

Option 1: Local Development

  1. Ensure PostgreSQL is running on your local machine.
  2. Create a database and update the credentials in src/main/resources/application.properties:
   spring.datasource.url=jdbc:postgresql://localhost:5432/interntrack
   spring.datasource.username=your_username
   spring.datasource.password=your_password
  1. Set the following environment variables:
    • MAIL_USERNAME / MAIL_PASSWORD — for email notifications (see Interview Reminders)
    • CLOUDINARY_CLOUD_NAME / CLOUDINARY_API_KEY / CLOUDINARY_API_SECRET — for CV file storage (see File Upload)
    • GEMINI_API_KEY — for AI gap analysis (see AI Gap Analyzer)
  2. Run the application using Maven:
   mvn spring-boot:run
  1. The API will be available at http://localhost:8080.
  2. Swagger UI: http://localhost:8080/swagger-ui.html

Option 2: GitHub Codespaces

  1. Go to the repository on GitHub.
  2. Add MAIL_USERNAME, MAIL_PASSWORD, CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET, and GEMINI_API_KEY as Codespaces repository secrets so they're injected automatically.
  3. Click Code > Codespaces > Create codespace on main.
  4. The container automatically installs Java 17, PostgreSQL, and Redis.
  5. Run the application with the codespaces profile to enable Redis-backed caching:
   ./mvnw spring-boot:run -Dspring-boot.run.profiles=codespaces
  1. Open the forwarded port 8080 URL and append /swagger-ui.html.

API Endpoints

Method Path Description
POST /api/auth/register Registers a new user.
POST /api/auth/login Authenticates a user and returns a JWT token.
DELETE /api/auth/delete-account Deletes the authenticated user's account and all their applications. Requires password confirmation. (requires token)
POST /api/applications Creates a new internship application, owned by the authenticated user. (requires token)
GET /api/applications Retrieves all applications belonging to the authenticated user. (requires token)
GET /api/applications/{id} Retrieves a specific application by ID, if it belongs to the authenticated user. (requires token)
PUT /api/applications/{id} Updates an existing application, if it belongs to the authenticated user. (requires token)
DELETE /api/applications/{id} Deletes an application, if it belongs to the authenticated user. (requires token)
GET /api/applications/dashboard Returns total application count and a status breakdown, scoped to the authenticated user. (requires token)
POST /api/applications/{id}/upload-cv Uploads a PDF CV for the given application, replacing any previously uploaded file. (requires token)
GET /api/applications/{id}/download-cv Downloads the CV file attached to the given application. (requires token)
DELETE /api/applications/{id}/delete-cv Deletes the CV file attached to the given application. (requires token)
POST /api/applications/{id}/analyze Runs (or returns a cached) AI gap analysis comparing the job description against the uploaded CV. (requires token)

Example Request Body (POST / PUT for /api/applications)

{
  "companyName": "Google",
  "position": "Backend Developer Intern",
  "status": "Applied",
  "appliedDate": "2026-07-17",
  "interviewDate": "2026-07-25",
  "notes": "Referral used.",
  "jobDescription": "We are looking for a Backend Developer Intern with experience in Java, Spring Boot, and PostgreSQL..."
}

interviewDate and jobDescription are optional — leave them out (or set to null) until an interview is scheduled or you're ready to run an analysis.

Example Response

{
  "id": 1,
  "companyName": "Google",
  "position": "Backend Developer Intern",
  "status": "Applied",
  "appliedDate": "2026-07-17",
  "interviewDate": "2026-07-25",
  "notes": "Referral used.",
  "jobDescription": "We are looking for a Backend Developer Intern with experience in Java, Spring Boot, and PostgreSQL...",
  "aiInsight": null,
  "aiInsightGeneratedAt": null
}

Dashboard & Caching

The /api/applications/dashboard endpoint returns the total number of applications and a dynamic breakdown by status (grouped by whatever status values actually exist in the database, rather than a fixed set), scoped to the authenticated user.

{
  "total": 2,
  "statusCounts": {
    "Applied": 2
  }
}

Results are cached to avoid recomputing the breakdown on every request. The cache is invalidated automatically whenever an application is created, updated, or deleted. The application uses Spring's Cache abstraction, so the same @Cacheable/@CacheEvict code works with two different backends depending on the environment:

  • Local development: in-memory Simple Cache (no external dependency required).
  • GitHub Codespaces: Redis, running in the container.

Since dashboard data is scoped per user, the cache key is tied to the authenticated username — each user gets their own cached breakdown, and one user's dashboard is never served to another.

Interview Reminders

A scheduled job runs every 5 minutes and checks for applications with an interviewDate set to today or tomorrow. For each match, it sends a reminder email — via Gmail SMTP using JavaMailSender — to the application owner's registered email address.

To avoid sending duplicate reminders, each application tracks a lastReminderSentDate. This is only updated after a reminder email is confirmed sent successfully; if the email fails (e.g. an SMTP error), the send is retried on the next scheduled run instead of being silently skipped.

Mail credentials are never hardcoded or committed to the repository. They're read from environment variables:

spring.mail.username=${MAIL_USERNAME}
spring.mail.password=${MAIL_PASSWORD}

MAIL_PASSWORD should be a Gmail App Password, generated separately from your main account password, so it can be revoked independently if ever exposed.

Note: On the deployed Render instance, outbound SMTP connections on port 587 are currently blocked by Render's free-tier network restrictions, so reminder emails are not delivered in production. The feature works correctly when run locally or in Codespaces with MAIL_USERNAME/MAIL_PASSWORD set.

File Upload (CV)

Each application can have one PDF CV attached to it, uploaded via POST /api/applications/{id}/upload-cv.

  • Only PDF files are accepted; anything else returns a 400 Bad Request.
  • Uploading a new file for an application that already has one automatically replaces the old file — there's no separate "update" endpoint, just upload again.
  • DELETE /api/applications/{id}/delete-cv removes the file without uploading a replacement.
  • Deleting an application also deletes its attached CV file.
  • All of the above respect ownership: you can only upload, download, or delete a CV for an application that belongs to you.

Files are stored on Cloudinary rather than local disk, so they persist across redeploys — including on Render's free tier, where the container's local filesystem is wiped on every redeploy. Credentials are read from environment variables and never committed to the repository:

cloudinary.cloud-name=${CLOUDINARY_CLOUD_NAME}
cloudinary.api-key=${CLOUDINARY_API_KEY}
cloudinary.api-secret=${CLOUDINARY_API_SECRET}

AI Gap Analyzer

POST /api/applications/{id}/analyze compares the jobDescription text stored on that specific application against the CV actually uploaded for that same application — sent together to the Gemini API (gemini-3.8-flash) using its multimodal input, so the model reads the PDF directly rather than relying on a fixed, hardcoded skill list. The response highlights the 2–3 most relevant gaps between the two and explains why each one matters for that specific role, then stores the result on the application as aiInsight, with a timestamp in aiInsightGeneratedAt.

  • Isolated per application: each application has its own jobDescription and cvFilePath, so analyzing application #1 never touches application #2's data, even if they share the same CV.
  • Cached, not re-run needlessly: if aiInsight is already populated, calling /analyze again returns the cached result immediately instead of calling Gemini again.
  • Automatically invalidated — aiInsight is cleared and a fresh analysis will be generated next time /analyze is called — whenever:
    • the jobDescription is updated,
    • a new CV is uploaded, or
    • the CV is deleted.
  • Fails safely: if the Gemini API is unavailable or returns an error, the request returns a 503 Service Unavailable with a clear message — it never crashes the request or corrupts existing data.

Requires a GEMINI_API_KEY (free, from Google AI Studio, no billing needed):

gemini.api.key=${GEMINI_API_KEY}

Example Response (POST /api/applications/{id}/analyze)

Example AI gap analysis response

Testing

Core business logic in ApplicationService is covered by unit tests (JUnit 5 + Mockito), focused on the rules that matter most for correctness and safety:

  • Ownership isolation — fetching an application that belongs to another user correctly results in a 404, never leaking data across accounts.
  • AI insight invalidation — updating the job description, uploading a new CV, or deleting a CV clears any existing aiInsight, while an unrelated update leaves a valid analysis untouched.
  • Gap analyzer caching — a cached aiInsight is returned without calling the Gemini API again; a missing one triggers a real call.
  • Input validation for /analyze — missing job description or missing CV both fail with a clear 400, before any external API call is attempted.

Run the tests with:

mvn test

API Documentation (Swagger)

Interactive API documentation is available via Swagger UI at /swagger-ui.html. All protected endpoints can be tested directly from the browser after authorizing with a JWT token (obtained from /api/auth/login) via the Authorize button.

Swagger endpoint list

Validation

Requests with missing or invalid fields (companyName, position, status, appliedDate) are handled by a centralized GlobalExceptionHandler. It intercepts validation failures and returns a structured 400 Bad Request response detailing each failing field, instead of a generic server error.

Example Error Response

{
  "timestamp": "2026-07-28T14:30:15.12345",
  "status": 400,
  "errors": {
    "companyName": "Company name cannot be blank",
    "appliedDate": "Applied date cannot be in the future"
  }
}

Requests to non-existent resources (e.g., GET /api/applications/999) — or to a resource that belongs to a different user — return a 404 Not Found with a similarly structured error message.

Authentication

The API uses JWT (JSON Web Token) based authentication. All endpoints under /api/applications require a valid token; only registration and login are publicly accessible.

Register

POST /api/auth/register
{
  "username": "your_username",
  "email": "your_email@example.com",
  "password": "your_password"
}

Login

POST /api/auth/login
{
  "username": "your_username",
  "email": "your_email@example.com",
  "password": "your_password"
}

Returns a JWT token on success:

{
  "token": "eyJhbGciOiJIUzM4NCJ9..."
}

Using the Token

Include the token in the Authorization header for all protected endpoints:

Authorization: Bearer <your_token>

Requests without a valid token return a 401 Unauthorized response:

{
  "timestamp": "2026-08-07T11:27:30.787292700",
  "status": 401,
  "message": "Authentication required. Please provide a valid token."
}

Deleting Your Account

DELETE /api/auth/delete-account
Authorization: Bearer <your_token>
{
  "password": "your_password"
}

Permanently deletes the authenticated user's account along with all of their application records. The account to delete is always derived from the JWT token, never from the request body — so a valid token for one user can never be used to delete another user's account. Requires the correct current password in the request body; an incorrect password returns 401 Unauthorized.

Known Limitations

  • Production email delivery: Render's free tier blocks outbound SMTP on port 587, so interview reminder emails are not sent when running on the deployed instance. This works correctly locally or in Codespaces.
  • AI provider availability: Gemini's free tier occasionally returns a temporary 503 under high demand on Google's side; the API surfaces this as a 503 rather than retrying automatically, so an occasional manual retry may be needed.

About

A Spring Boot REST API for tracking internship applications — JWT auth with per-user isolation, automated interview reminder emails, Cloudinary-backed CV storage, and Gemini-powered AI gap analysis comparing each CV against its job description.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages