Skip to content

Repository files navigation

INFNOVA Internship Applicant Management API

Backend Internship practical challenge submission. A NestJS + Prisma API for managing internship applications, with JWT-authenticated admin access.

Tech stack

  • NestJS (TypeScript) — modular controller/service/DTO architecture
  • Prisma ORM against MariaDB/MySQL (also works against PostgreSQL or SQLite with a one-line provider change)
  • JWT bearer auth via @nestjs/jwt + passport-jwt, passwords hashed with bcrypt
  • class-validator / class-transformer for request validation
  • Swagger/OpenAPI via @nestjs/swagger
  • Jest for unit and e2e tests

Setup

npm install
cp .env.example .env

Then edit .env:

  • Set DATABASE_URL to your MariaDB/MySQL connection string:
    DATABASE_URL="mysql://<user>:<password>@localhost:3306/<database>"
    
  • Set JWT_SECRET to a real random string (e.g. openssl rand -hex 32).

Create the database and a dedicated user in MariaDB before migrating, e.g.:

CREATE DATABASE infnova_applicants;
CREATE USER 'infnova'@'localhost' IDENTIFIED BY 'your-password';
GRANT ALL PRIVILEGES ON infnova_applicants.* TO 'infnova'@'localhost';

Note: prisma migrate dev creates a temporary shadow database to validate migrations safely, which requires the DB user to be able to create/drop databases — not just query the one it owns. For local development this project's user was granted broader privileges (GRANT ALL PRIVILEGES ON *.* ... WITH GRANT OPTION) to support this. A production deployment would instead use prisma migrate deploy (which doesn't need a shadow database) with a narrowly scoped user.

Then run:

npx prisma migrate dev --name init   # creates the tables
npm run prisma:seed                  # creates an admin user + sample applicants
npm run start:dev

The API runs at http://localhost:3000. Swagger docs are at http://localhost:3000/api/docs.

Seeded admin login (from .env):

  • email: SEED_ADMIN_EMAIL (default admin@infnova.test)
  • password: SEED_ADMIN_PASSWORD (default ChangeMe123!)

Switching database providers

Change provider in prisma/schema.prisma (mysql, postgresql, or sqlite), point DATABASE_URL at the corresponding instance, then re-run npx prisma migrate dev.

Auth flow

  1. POST /api/auth/login with { email, password } → returns accessToken.
  2. Send Authorization: Bearer <accessToken> on every other request. All /api/applicants/* and /api/dashboard/* routes are guarded.
  3. GET /api/auth/me returns the authenticated admin's profile.

There is no self-service admin registration endpoint by design — admins are provisioned via the seed script (or directly in the DB), since the brief only asked for admin login, not admin sign-up.

Endpoints

Method Path Description
POST /api/auth/login Admin login, returns JWT
GET /api/auth/me Current admin profile
POST /api/applicants Create applicant
GET /api/applicants Paginated list — page, limit, search, status, track, sortBy, sortOrder
GET /api/applicants/:id Get one applicant
PATCH /api/applicants/:id Update applicant details
PATCH /api/applicants/:id/status Update status (enforces business rule)
PATCH /api/applicants/:id/notes Replace internal notes (≤1000 chars)
DELETE /api/applicants/:id Soft-delete
GET /api/dashboard/summary Totals by status and track (excludes deleted)

Architecture

  • src/prismaPrismaService, exposed as a global module so it's injectable anywhere without re-importing.
  • src/auth — login, JWT strategy/guard, @CurrentUser() decorator.
  • src/applicants — controller (routing only), service (all business logic), DTOs (one per operation, so validation rules are scoped to exactly what that request needs).
  • src/dashboard — read-only summary stats, uses Prisma groupBy.
  • src/common — the global exception filter and the shared pagination DTO.

Controllers stay thin (auth guard + DTO binding only); all business rules (status-transition check, soft-delete filtering, uniqueness) live in the services, as requested in the brief.

Business rules and where they're enforced

  • Unique email — enforced at the DB level (@unique in the Prisma schema) and surfaced as a 409 Conflict by the global exception filter.
  • Notes ≤ 1000 chars@MaxLength(1000) in UpdateNotesDto.
  • Rejected → Accepted blocked — a single table of blocked transitions in ApplicantsService.updateStatus. All other transitions, including re-opening a rejected applicant back to Pending/Shortlisted, are allowed.
  • Soft deletedeletedAt timestamp instead of a row delete. Every read path (findAll, findOne, and therefore every dashboard count) filters deletedAt: null.
  • Auth required for writes/reads of applicant dataJwtAuthGuard on the whole ApplicantsController and DashboardController.

Testing

npm run test        # unit tests (auth + applicants business logic)
npm run test:e2e     # e2e: validation + auth-guard smoke tests
npm run test:cov     # coverage report

Unit tests focus on the two things most likely to be graded closely: the Rejected→Accepted block (including that every other transition is still allowed) and login success/failure paths. E2E tests confirm the global validation pipe and JWT guard are actually wired up on the running app.

All endpoints were also manually verified through Swagger UI (/api/docs), including the auth flow, full applicant CRUD, the blocked status transition, and soft-delete exclusion from both the list and dashboard summary.

Assumptions & known limitations

  • Only one role exists (Admin) — there's no separate "viewer" role, since the brief only mentions administrators.
  • Because email uniqueness is enforced at the DB level and soft-deleted rows aren't removed, a soft-deleted applicant's email can't currently be reused for a new applicant. A follow-up would be a partial unique index scoped to deletedAt IS NULL. Flagging this as a known limitation rather than solving it here.
  • PATCH /api/applicants/:id intentionally excludes status — status only changes through the dedicated /status endpoint so the transition rule can't be bypassed via the general update route.
  • No refresh tokens / token revocation — a single JWT with an expiry (JWT_EXPIRES_IN) was sufficient for the scope of this challenge.
  • Search on name/email uses a contains match. MariaDB's default collation (utf8mb4_general_ci) makes this case-insensitive out of the box; no extra configuration was needed for that here.
  • The local dev database user has broad privileges (see the shadow-database note above) purely to support prisma migrate dev. A production setup would scope this down and use prisma migrate deploy instead.

Environment variables

See .env.example. JWT_SECRET and SEED_ADMIN_PASSWORD in that file are placeholders only — replace them locally; nothing real is committed.

About

No description, website, or topics provided.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages