Backend — Overview

The pakAG backend is a Next.js 15 App Router application that exclusively uses Route Handlers as its API layer. There are no pages, no React server components serving HTML — only JSON endpoints.

Tech stack

TechnologyRole
Next.js 15 (App Router)HTTP server + routing
TypeScriptType safety end-to-end
mysql2/promiseMySQL connection pool with prepared statements
jsonwebtokenAccess token signing and verification
bcryptPassword hashing
ResendTransactional email (activation, reset, status notifications)
Google Directions APIRoute optimization and stop ordering
Google Geocoding APIAddress → lat/lng conversion for package creation

Monorepo location

erronka2025_js/
├── backend-js/          ← THIS SERVICE
│   ├── src/app/api/     ← All Route Handlers
│   ├── src/app/lib/     ← Shared utilities (jwt, errors, response, dto guards)
│   ├── src/app/config/  ← Environment variables + DB pool singleton
│   ├── src/app/types/   ← Global domain enums and types
│   └── schema.sql       ← MySQL schema (source of truth, at repo root)
├── docs/                ← This documentation site
├── erronkaFrontend-React/  ← Admin frontend
└── frontend-app/        ← Distributor mobile app

Folder structure per endpoint

Every use-case lives in its own folder under src/app/api/{resource}/{action}/:

src/app/api/users/create/
├── dtos/
│   └── createUser.dto.ts    ← validateCreateUserDto(body: unknown): CreateUserDto
├── repository/
│   └── user.repo.ts         ← SQL only, prepared statements, no logic
├── service/
│   └── user.service.ts      ← Business logic, no SQL
├── types.ts                 ← CreateUserDto, CreateUserResponse interfaces
└── route.ts                 ← Orchestrates: requireAuth → validate → service → res.xxx

[!NOTE] The canonical reference implementation is src/app/api/users/create/. Read it first when learning the pattern. See Patterns → for a full walkthrough.

API resources

Auth (/api/auth/)

MethodPathAuth requiredDescription
POST/api/auth/loginCredentials → access token (body) + refresh token (HttpOnly cookie)
POST/api/auth/refreshCookie refresh_tokenRotate refresh token, issue new access token
POST/api/auth/logoutRevoke refresh token in DB
GET/api/auth/meBearer (any role)Return authenticated user
POST/api/auth/forgotPasswordSend password reset email (always 200, never leaks existence)
POST/api/auth/changePwdReset token (query param)Set new password with valid reset token
POST/api/auth/activateAccountActivation token (query param)Set password and activate new account

Users (/api/users/) — admin only

MethodPathDescription
POST/api/users/createCreate user, send activation email
GET/api/users/listList users with filters
GET/api/users/getByIdGet single user
PATCH/api/users/updateUpdate name/email/role/is_active
DELETE/api/users/removeDelete user (cannot self-delete)
PATCH/api/users/changeMyPwdChange own password (any role)

Packages (/api/packages/)

MethodPathAuthDescription
POST/api/packages/createadminCreate package + geocode address + issue tracking token + send email
GET/api/packages/listadminList with filters and pagination
GET/api/packages/getByIdadminGet single package
PATCH/api/packages/updateadminUpdate package and address
DELETE/api/packages/deleteadminDelete package
PATCH/api/packages/updateStatusdistributorAdvance status(es) + write log + send email
GET/api/packages/getMyPackagesdistributorPackages assigned to the authenticated distributor
GET/api/packages/getDailySummaryadminCount by status for today

Routes (/api/routes/)

MethodPathAuthDescription
POST/api/routes/createadminCreate optimized route via Google Directions
GET/api/routes/getByUserAndDateadminRoute by user + date
GET/api/routes/getMyDailydistributorAuthenticated distributor’s route for today
PATCH/api/routes/updateStatus/[id]admin | distributorUpdate route status
GET/api/routes/continueFromPastdistributorCheck if there is a pending past route
POST/api/routes/continueFromPastdistributorMigrate pending stops from yesterday to today

Stops (/api/stops/)

MethodPathAuthDescription
PATCH/api/stops/reorderadminReorder stops within a route
PATCH/api/stops/updateArrivaldistributorRecord actual arrival time for own stop

Logs (/api/logs/)

MethodPathAuthDescription
GET/api/logs/listAlladminAll status-change logs with filters
GET/api/logs/listByPackageadminLogs for a specific package

Tracking — public (/api/tracking/)

MethodPathAuthDescription
GET/api/tracking/[trackingToken]Public package status lookup by opaque token

The 4-layer pattern

Every endpoint is split across exactly four layers. This is a hard constraint, not a style preference:

request


route.ts          ← HTTP boundary: parse, orchestrate, return


dtos/             ← Validation only: typed interface + guard functions


service/          ← Business logic: rules, transitions, side effects


repository/       ← SQL only: prepared statements, row mapping


MySQL

The rules:

  • No SQL in route.ts, service/, or dtos/
  • No business logic in repository/
  • No Zod — validation uses guard functions from src/app/lib/dto.ts
  • route.ts always follows: requireAuth → validateDto → service → res.xxx

See Patterns → for detailed code examples.

Error types

All errors are thrown from src/app/lib/errors.ts and caught by handleError in route.ts:

ClassHTTP statusWhen to throw
ValidationError400Invalid or missing input in a DTO
UnauthorizedError401Missing or invalid token
ForbiddenError403Valid token but insufficient role
NotFoundError404Resource does not exist
ConflictError409Duplicate resource (e.g. email already taken)
// route.ts always ends with:
} catch (error) {
  return handleError('[POST /api/users/create]', error);
}

Never throw new Error() directly. Always use one of the five typed error classes.

Shared library modules

FileWhat it provides
lib/errors.tsFive typed error classes
lib/response.tsres.ok / created / noContent / validationError / notFound / conflict / serverError + handleError()
lib/jwt.tssignAccessToken, verifyAccessToken, extractBearerToken, requireAuth
lib/dto.tsType guards: isString, isEmail, isNumber, isBoolean, isPositiveInteger, isUserRole, isPackageValidStatus
lib/hashPasword.tsencryptPwd, verifyPassword (bcrypt)
lib/email/email.service.tsSend transactional emails via Resend
lib/maps/maps.service.tsoptimizeRoute, geocodeAddress (Google APIs)
lib/packageStatus/packageStatusSideEffects.service.tsInsert status log + dispatch tracking email

Global domain types

Defined in src/app/types/index.ts:

export const USER_ROLES = {
  admin: 'admin',
  distributor: 'distributor',
} as const;
export type UserRole = (typeof USER_ROLES)[keyof typeof USER_ROLES];
 
export const PACKAGE_STATUSES = {
  pending: 'pending',
  assigned: 'assigned',
  in_transit: 'in_transit',
  delivered: 'delivered',
  undelivered: 'undelivered',
  failed: 'failed',
} as const;
export type PackageStatus = (typeof PACKAGE_STATUSES)[keyof typeof PACKAGE_STATUSES];
 
export const TOKEN_TYPES = {
  refresh_token: 'refresh_token',
  tracking_token: 'tracking_token',
  reset_pwd_token: 'reset_pwd_token',
  activate_account_token: 'activate_account_token',
} as const;

Environment variables

Copy .env.example to .env.local and fill in all values before running:

# Database
MYSQL_HOST=localhost
MYSQL_USER=root
MYSQL_PASSWORD=yourpassword
MYSQL_DATABASE=erronka
MYSQL_PORT=3306

# JWT
JWT_SECRET=<minimum-32-char-random-secret>
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_DAYS=7

# Email
RESEND_API_KEY=re_xxxx

# Google Maps
GOOGLE_DIRECTIONS_API_KEY=AIzaSy_xxxx

# URLs for email links
TRACKING_BASE_URL=https://yourhost.example/tracking/
RESET_BASE_URL=https://yourhost.example/resetPassword/

# Tokens
TRACKING_EXPIRES_DAYS=30

# Users
DEFAULT_USER_PASSWORD=ChangeMe123!

[!WARNING] JWT_SECRET and DEFAULT_USER_PASSWORD have insecure hardcoded fallbacks in the current codebase. Always set these explicitly in production. The fallback secrets are public in the repository and cannot be trusted.

[!NOTE] The schema.sql at the repository root creates the database as erronka. The .env.example shows pakag. Use the name that matches your actual MySQL database — both names appear in different places.

MySQL connection pool

src/app/config/dbConfig.ts exposes a single connect() function that lazily initializes a mysql2 connection pool (limit: 10). All repositories call await connect() to acquire the pool.

SSL is enabled with rejectUnauthorized: false. This is appropriate for development with self-signed certificates; review for production.

Package status side effects

Every time a package status changes, applyPackageStatusSideEffects runs automatically:

  1. Inserts a row into package_status_logs
  2. Looks up the package’s tracking token
  3. Looks up the assigned distributor’s name
  4. Dispatches a status email to the recipient

The allowed transitions are:

pending     → assigned
assigned    → in_transit
in_transit  → delivered
in_transit  → undelivered
in_transit  → failed

Route optimization flow

When an admin creates a route (POST /api/routes/create):

  1. Verifies the target user has role distributor
  2. Merges carryover packages (assigned packages from previous days for that distributor)
  3. Combined list is capped at 20 stops
  4. Calls Google Directions API with optimize:true to reorder waypoints
  5. Computes estimated_arrival per stop from the response legs
  6. Inserts the route and stop rows, updates estimated_delivery on each package
  7. Returns the ordered route with total distance and duration

Further reading