Backend API for AutoPlan, a web application for processing geospatial data in engineering and cadastral surveys. It manages users, projects, and survey plans; runs the surveying computations (traverse, area, differential leveling); and delegates the actual plan drawing to the companion Python service (autoplan-python), which produces DXF/DWG/PDF bundles that can be edited in AutoCAD.
Built with TypeScript, Express 5, MongoDB (Mongoose), and Redis.
The codebase is a modular monolith: one folder per feature under src/modules, each owning
its routes, validation, controller, service (business logic), Mongoose model, and repository.
src/
├── app.ts Express app: middleware, routes, error handling
├── server.ts Bootstrap: DB/Redis connections, graceful shutdown
├── config/ Environment, MongoDB, and Redis setup
├── db/ Generic repository (soft deletes, ownership scoping) + query helpers
├── middlewares/ Authentication, request logging, global error handler
├── routes/index.ts Mounts every module router under /api/v1
├── services/ External integrations (AWS SES email)
├── templates/ Email templates (Handlebars)
├── utils/ Logger, errors, responses, validation, JWT, caches
└── modules/
├── auth/ OTP + Google sign-in, sessions (JWT + Redis api token)
├── user/ Profiles
├── project/ Survey projects (client, location, surveyor, status)
├── plan/ Survey plans: data editing, generation, computation import/convert
├── traverse/ Back / forward / traverse computations, bearings, area
└── leveling/ Differential leveling (rise-and-fall & height-of-instrument)
Request flow: route → validation (validatorjs) → controller (HTTP concerns) → service
(business logic) → repository (MongoDB). Errors are thrown as ApiError anywhere in the
chain and rendered by the global error handler.
Response envelope (all endpoints): { code, error, message?, data? }.
Authentication uses two credentials together: a signed, AES-encrypted JWT
(Authorization: Bearer <token>) and a revocable Redis-backed token (x-api-token).
Logging out revokes the api token.
A background generation reads every point of a survey out of MongoDB, writes them to object storage as NDJSON, and hands the engine a link. The engine then thins them on arrival, keeping one point per printed millimetre of the sheet, and draws from what is left.
For a 1.5-million-point survey that is a 113 MB export and roughly a minute of database reads to retain about 4,400 points -- 0.3% of what was moved. The transfer exists only so the engine can decide what to discard.
The decision does not need the engine. It is:
cell = 1.0mm x scale / 1000 # metres of ground per printed millimetre
key = (floor(easting / cell), floor(northing / cell))
and the first point to land in a cell wins. plan.scale is here already, so
the API could thin before shipping.
Two ways to do it, and they are not equally worthwhile:
- While streaming out of the repository. A set of cell keys as points are read. Cuts the upload from 113 MB to well under 1 MB, but still reads every document -- and the read is the slow half.
- In MongoDB.
$unwindthe buckets,$groupby cell,$firstper group, so only survivors leave the database. Removes the read and the transfer.
Option 2 is the one worth having, with two caveats. $first follows pipeline
order and traverse order is meaningful here -- the engine keeps the first point
walked, which is what makes recovered ids and station order mean anything --
so it needs an explicit $sort on bucket seq before the group. And the sheet
may be auto-fitted to a coarser scale than the one requested, which makes the
cell larger; thinning at the requested scale keeps a superset of what the
engine would, which is safe, whereas thinning coarser would drop points it
would have kept.
The cost is that the rule would then exist twice, in TypeScript and in Python
(point_stream.GridDecimator). Worth a test that runs a fixture through both
and asserts they retain the same points, or they will drift.
- Back computation — distances, whole-circle bearings, and (optionally) parcel area from known coordinates (shoelace method).
- Forward computation — station coordinates from bearing/distance legs, with linear misclosure detection and optional correction distributed by cumulative arithmetic sums.
- Traverse computation — carries bearings through observed angles from known control, distributes angular misclosure across legs when a check leg closes the traverse, then runs a forward computation for coordinates.
- Differential leveling — rise-and-fall or height-of-instrument reduction, with optional misclosure correction distributed per instrument setup.
tests/computations.test.ts locks these engines against golden fixtures — run npm test
after touching any of them.
All routes are mounted under /api/v1. 🔒 = requires authentication.
| Area | Route | Description |
|---|---|---|
| Auth | POST /auth/login/otp |
Email an OTP (creates the account on first login) |
POST /auth/login |
Exchange email + OTP for a session | |
POST /auth/login/google |
Sign in with a Google ID token | |
GET /auth/logout 🔒 |
Revoke the current api token | |
| User | POST /user/profile/set 🔒 · GET /user/profile/fetch 🔒 |
Profile management |
| Project | POST /project/create 🔒 · GET /project/list 🔒 · GET /project/fetch/:id 🔒 · PUT /project/edit/:id 🔒 · DELETE /project/delete/:id 🔒 |
Project CRUD |
| Plan | POST /plan/create 🔒 · GET /plan/list/:project_id 🔒 · GET /plan/fetch/:plan_id 🔒 · PUT /plan/edit/:plan_id 🔒 · DELETE /plan/delete/:plan_id 🔒 |
Plan CRUD |
PUT /plan/{coordinates,elevations,parcels}/edit/:plan_id 🔒 |
Survey data | |
PUT /plan/topo/{boundary,setting}/edit/:plan_id 🔒 |
Topographic data | |
PUT /plan/layout/boundary/edit/:plan_id 🔒 |
Layout perimeter (auto-closed ring) | |
PUT /plan/layout/params/edit/:plan_id 🔒 |
Layout design parameters (plot module, roads, blocks, reserves) | |
PUT /plan/layout/data/edit/:plan_id 🔒 |
Designed layout data: coordinate register, plots, roads | |
PUT /plan/route/longitudinal/params/edit/:plan_id 🔒 |
Route profile parameters | |
PUT /plan/route/params/edit/:plan_id 🔒 |
Route plan-view parameters (right-of-way, toggles) | |
PUT /plan/{traverse,forward,differential-leveling}-data/edit/:plan_id 🔒 |
Field computations | |
GET /plan/generate/:plan_id 🔒 |
Generate the drawing (Python service) | |
PUT /plan/computation/convert/:plan_id 🔒 |
Convert a computation into a plan | |
PUT /plan/import/:plan_id 🔒 |
Import a computation into a plan | |
| Compute | POST /traverse/back-computation · POST /traverse/forward-computation · POST /traverse/traverse-computation · POST /traverse/area-computation |
Stateless traverse computations |
POST /leveling/differential |
Stateless leveling reduction |
Requirements: Node.js 22+, MongoDB, Redis.
npm install
cp .env.example .env # fill in your values
npm run dev # ts-node + nodemon
npm test # computation regression tests
npm run build && npm start| Variable | Purpose |
|---|---|
PORT |
HTTP port (default 3000) |
MONGO_URI |
MongoDB connection string |
REDIS_URI |
Redis connection string |
JWT_SECRET |
EC (ES256) private key in PEM format, \n-escaped |
ENCRYPTION_KEY |
32-byte hex key for AES-256-GCM payload encryption |
GOOGLE_CLIENT_ID |
OAuth client id for Google sign-in |
PYTHON_SERVER |
Base URL of the plan-drawing Python service |
ALLOWED_ORIGINS |
Comma-separated CORS origins, or * |
AWS_ACCESS_KEY / AWS_SECRET_KEY / AWS_REGION / AWS_SES_SENDER |
SES credentials for OTP emails |
Pushes to main trigger .github/workflows/prod.yml: the Docker image is built and pushed
to Docker Hub, then deployed to the production Ubuntu server over SSH with Docker Compose.
Required secrets: DOCKER_USERNAME, DOCKER_PASSWORD, SERVER_HOST, SERVER_USERNAME,
SERVER_SSH_KEY, SERVER_PORT.
MIT — see LICENSE.