Skoolsewa - Ecommerce Docs
Developer ResourcesGeography

Geography API Reference

Complete API contracts for Nepal's administrative geography lookup — provinces, districts, municipalities, routes, auth, DTOs, and errors.

Geography - API Reference

Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin-facing API owned by GeographyController, inside LookupsModule. No public or mobile-facing route exists.

1. Documentation Evidence

AreaFiles InspectedWhat Was Verified
Controllerapps/api/src/modules/lookups/geography/geography.controller.tsRoutes, methods, guards, permissions, decorators.
DTOsapps/api/src/modules/lookups/dto/geography.dto.tsRequest, query, response, validation, defaults.
Servicesgeography-provinces.service.ts, geography-districts.service.ts, geography-municipalities.service.tsBehavior, error translation.
Moduleapps/api/src/modules/lookups/lookups.module.tsWiring and dependency on RoleModule.
Schemapackages/db/src/schema/school/geography.tsColumns, indexes, CHECK constraints.
Errorsapps/api/src/common/types/error-codes.ts (around lines 198-206)Every error code this module can produce.
Permissionspackages/db/src/authorization/permission-catalog.ts (around line 100)Geography declared as a permission module.

2. Module Summary

FieldValue
Module nameGeographyController (inside LookupsModule)
Module sluggeography
Primary actorsAdmin (only role verified to hold any Geography_* permission in this codebase)
API surfaceAdmin only — no @Public() route on the controller
Base route prefix/api/lookups/provinces, /api/lookups/districts, /api/lookups/municipalities (global prefix api set in apps/api/src/main.ts; controller declares lookups locally)
Auth modelJwtAuthGuard + RoleGuard, applied at the controller class level
PersistencePostgreSQL, tables provinces, districts, municipalities; no cache layer
Sibling docsBackend, Features and flows

3. Concepts and Terminology

TermMeaningSource FileUsed By
ProvinceNepal's top administrative tier — 7, nationally fixed.geography.tsEvery district, and a person/school address.
DistrictThe middle tier — 77, nationally fixed, each belonging to exactly one province.geography.tsEvery municipality, and a person/school address.
Municipality (local level)The lowest tier this module tracks — up to 753 nationally, of which 36 are seeded. Typed as metropolitan, sub_metropolitan, municipality, rural_municipality, or the historical vdc.geography.tsA person/school address, the leaf of the hierarchy.
Retirement (isActive)Removes a row from the default (active-only) pick list without touching any existing reference to it. There is no deleted_at on any of the three tables.geography.tsisActive/includeInactive on every route.
Rename/reparent refusalRefuses to change a row's name or parent when it is already referenced — see Section 5.All three servicesPATCH on every tier.
Public ID (publicId)The UUID every PATCH/DELETE route addresses a row by. The internal integer id is still returned in the response body and is what a child row's provinceId/districtId field carries.geography.tsEvery route param below.

4. API Surface Map

SurfaceMethodPathActorPermissionPurpose
AdminGET/api/lookups/provincesAdminGeography_READList provinces (unpaginated, 7 rows).
AdminPOST/api/lookups/provincesAdminGeography_CREATEAdd a province.
AdminPATCH/api/lookups/provinces/:publicIdAdminGeography_UPDATERename, recode, or retire a province.
AdminDELETE/api/lookups/provinces/:publicIdAdminGeography_DELETEDelete an unused province.
AdminGET/api/lookups/districtsAdminGeography_READList districts (unpaginated, 77 rows), optionally filtered by provinceId.
AdminPOST/api/lookups/districtsAdminGeography_CREATEAdd a district.
AdminPATCH/api/lookups/districts/:publicIdAdminGeography_UPDATERename, reparent, or retire a district.
AdminDELETE/api/lookups/districts/:publicIdAdminGeography_DELETEDelete an unused district.
AdminGET/api/lookups/municipalitiesAdminGeography_READList municipalities (paginated, can reach 753 nationally), optionally filtered by districtId.
AdminPOST/api/lookups/municipalitiesAdminGeography_CREATEAdd a municipality.
AdminPATCH/api/lookups/municipalities/:publicIdAdminGeography_UPDATERename, reparent, or retire a municipality.
AdminDELETE/api/lookups/municipalities/:publicIdAdminGeography_DELETEDelete an unused municipality.

All twelve routes are verified against geography.controller.ts, which declares exactly these and no others.

5. The Rename/Reparent and Delete Refusal Rules

5.1 Rename or reparent a referenced row

RuleEnforced ByWhy
A province, district or municipality already referenced by a child row, the school profile, or a person's address cannot be renamed.isReferenced pre-check in each service's updateThe export writes place names, not surrogate ids. A rename would silently rewrite the content of every export already downloaded and repoint every future name-resolved import.
A district or municipality already referenced cannot be reparented (provinceId/districtId changed).Same pre-check, also gating isReparentingReparenting breaks the composite foreign key from every referencing address in the same way a rename does.
Retirement (isActive: false/true) is never refused for this reason.Not gated by isReferenced at allRetirement does not change what the name says — it only removes the row from active pick lists.

5.2 Delete a referenced row

RuleEnforced ByWhy It Is a Pre-Check, Not a Caught Constraint
A row referenced by a child row, the school profile, or a person's address cannot be hard-deleted.isReferenced pre-check in each service's delete, over an enumerated column listThis repo's established pattern for foreign-key-guarded deletes, and because an explicit ON DELETE RESTRICT raises SQLSTATE 23001, not 23503 — a caught-code translator written for 23503 would never fire, and the delete would surface as an unmapped 500.
A row referenced only by a soft-deleted person is still undeletable.isReferenced deliberately does not filter users.deleted_atON DELETE RESTRICT counts every row regardless of the person's own soft-delete state; a liveness-filtered check would promise a delete the database then refuses.

6. DTO and Model Reference

6.1 ProvinceDto (response)

FieldTypeRequiredSource
idnumberYesInternal serial id.
publicIdstring (UUID)Yes
namestringYese.g. "Bagmati".
nameNpstring | nullOptionalThe Devanagari label.
codestring | nullOptional"P1".."P7", optional.
isActivebooleanYes
createdAt / updatedAtDateYes

6.2 DistrictDto (response)

Same fields as ProvinceDto plus provinceId (number) and provinceName (string) — the parent is always denormalised alongside the id so a list can render without a second round trip.

6.3 MunicipalityDto (response)

Same base fields plus districtId (number), districtName (string), type (enum, see below), and wardCount (number \| null — narrows the ward picker where known; never constrains the ward number actually stored on an address).

MUNICIPALITY_TYPES: metropolitan, sub_metropolitan, municipality, rural_municipality, vdc. vdc is offered even though it is seeded zero times — a school may still hold a historical address recorded that way.

6.4 List Query DTOs

None of the three extends the shared QueryDto. Provinces and districts are fixed, unpaginated lists — there is no pagination flag to toggle, so QueryDto's trio would advertise a parameter with no effect. Municipalities paginate, but the sort is fixed (name ASC, id ASC everywhere in this module) rather than caller-chosen, so sort/order/search are left off rather than inherited and ignored.

DTOFields
ListProvincesQueryDtoincludeInactive?: boolean
ListDistrictsQueryDtoprovinceId?: number, includeInactive?: boolean
ListMunicipalitiesQueryDtodistrictId?: number, includeInactive?: boolean, pagination?: boolean (default true), page?: number (default 1), size?: number (default 20, max 100)

6.5 Write DTOs

DTORequired FieldsOptional FieldsNotes
CreateProvinceDtoname (≤120 chars, trimmed)nameNp (≤120), code (≤20), isActive (default true)
UpdateProvinceDtoname, nameNp, code, isActiveRefused with GEOGRAPHY_RENAME_REFERENCED when renaming a referenced province.
CreateDistrictDtoprovinceId (int), name (≤120)nameNp, isActive
UpdateDistrictDtoprovinceId, name, nameNp, isActiveReparenting or renaming a referenced district is refused the same way.
CreateMunicipalityDtodistrictId (int), name (≤120), type (enum)nameNp, wardCount (1-35), isActivewardCount's upper bound is deliberately generous — Kathmandu Metropolitan City has 32 wards — because it exists to reject a typo of 320, not to encode a national maximum a boundary redraw would invalidate.
UpdateMunicipalityDtodistrictId, name, nameNp, type, wardCount, isActiveSame reparent/rename refusal.

7. Endpoint Reference

7.1 GET /api/lookups/provinces

Purpose: List all provinces. Unpaginated — 7 rows, nationally fixed.

Auth: Geography_READ.

Query: includeInactive (default: active only).

Response:

{
  "message": "Provinces fetched.",
  "data": [
    {
      "id": 3,
      "publicId": "018f2a1e-6b1e-7c3a-9d2e-1a2b3c4d5e6f",
      "name": "Bagmati",
      "nameNp": "बागमती",
      "code": "P3",
      "isActive": true,
      "createdAt": "2026-01-01T00:00:00.000Z",
      "updatedAt": "2026-01-01T00:00:00.000Z"
    }
  ],
  "errorCode": null
}

Errors: 401 AUTH_UNAUTHENTICATED, 403 PERMISSION_INSUFFICIENT.

7.2 POST /api/lookups/provinces

Purpose: Add a province.

Auth: Geography_CREATE.

Body: CreateProvinceDto.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_CREATE.
400VALIDATION_FAILEDInvalid body.
409GEOGRAPHY_NAME_TAKENA province with that name already exists (case-insensitive).

7.3 PATCH /api/lookups/provinces/:publicId

Purpose: Rename, recode, or retire a province.

Auth: Geography_UPDATE.

Body: UpdateProvinceDto.

Behavior: Renaming is refused with GEOGRAPHY_RENAME_REFERENCED when a district, the school profile, or a person's address already points at this province. Retirement (isActive: false) stays available regardless.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_UPDATE.
404PROVINCE_NOT_FOUNDNo province with that publicId.
400VALIDATION_FAILEDInvalid body.
409GEOGRAPHY_NAME_TAKENRenamed to a name already in use.
409GEOGRAPHY_RENAME_REFERENCEDRenaming a province a district, the school profile, or a person's address already points at.

7.4 DELETE /api/lookups/provinces/:publicId

Purpose: Delete an unused province.

Auth: Geography_DELETE.

Behavior: Permitted only when no district, school profile, or person's address references it. Retire with isActive=false instead.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_DELETE.
404PROVINCE_NOT_FOUNDNo province with that publicId.
409GEOGRAPHY_IN_USEA district, the school profile, or a person's address still points at it.

7.5 GET /api/lookups/districts

Purpose: List districts. Unpaginated — 77 rows, nationally fixed.

Auth: Geography_READ.

Query: provinceId (restrict to one province), includeInactive.

Response: Same shape as provinces, plus provinceId and provinceName on each row.

Errors: 401 AUTH_UNAUTHENTICATED, 403 PERMISSION_INSUFFICIENT.

7.6 POST /api/lookups/districts

Purpose: Add a district.

Auth: Geography_CREATE.

Body: CreateDistrictDto.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_CREATE.
400VALIDATION_FAILEDInvalid body.
404PROVINCE_NOT_FOUNDprovinceId does not exist.
409GEOGRAPHY_NAME_TAKENA district with that name already exists in that province.

7.7 PATCH /api/lookups/districts/:publicId

Purpose: Rename, reparent, or retire a district.

Auth: Geography_UPDATE.

Body: UpdateDistrictDto.

Behavior: Renaming or moving the district to a different province is refused with GEOGRAPHY_RENAME_REFERENCED when a municipality, the school profile, or a person's address already points at it. Retirement stays available regardless.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_UPDATE.
404DISTRICT_NOT_FOUNDNo district with that publicId.
404PROVINCE_NOT_FOUNDReparenting to a provinceId that does not exist.
400VALIDATION_FAILEDInvalid body.
409GEOGRAPHY_NAME_TAKENRenamed to a name already in use in the target province.
409GEOGRAPHY_RENAME_REFERENCEDRenaming or reparenting a district a municipality, the school profile, or a person's address already points at.

7.8 DELETE /api/lookups/districts/:publicId

Purpose: Delete an unused district.

Auth: Geography_DELETE.

Behavior: Permitted only when no municipality, school profile, or person's address references it.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_DELETE.
404DISTRICT_NOT_FOUNDNo district with that publicId.
409GEOGRAPHY_IN_USEA municipality, the school profile, or a person's address still points at it.

7.9 GET /api/lookups/municipalities

Purpose: List municipalities. Paginated — the national count can reach 753.

Auth: Geography_READ.

Query: districtId (restrict to one district — the common case, keeping most callers well under a page), includeInactive, pagination (default true), page (default 1), size (default 20, max 100).

Response: Same shape as districts, with districtId/districtName in place of provinceId/provinceName, plus type and wardCount. When pagination=false, the response is capped at PaginationUtil.UNPAGINATED_HARD_CAP (1000).

Errors: 401 AUTH_UNAUTHENTICATED, 403 PERMISSION_INSUFFICIENT, 400 VALIDATION_FAILED.

7.10 POST /api/lookups/municipalities

Purpose: Add a municipality — the supported path for the 717 local levels the seed does not carry.

Auth: Geography_CREATE.

Body: CreateMunicipalityDto.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_CREATE.
400VALIDATION_FAILEDInvalid body, including wardCount outside 1-35.
404DISTRICT_NOT_FOUNDdistrictId does not exist.
409GEOGRAPHY_NAME_TAKENA municipality with that name already exists in that district.

7.11 PATCH /api/lookups/municipalities/:publicId

Purpose: Rename, reparent, or retire a municipality.

Auth: Geography_UPDATE.

Body: UpdateMunicipalityDto.

Behavior: Renaming or moving the municipality to a different district is refused with GEOGRAPHY_RENAME_REFERENCED when the school profile or a person's address already points at it.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_UPDATE.
404MUNICIPALITY_NOT_FOUNDNo municipality with that publicId.
404DISTRICT_NOT_FOUNDReparenting to a districtId that does not exist.
400VALIDATION_FAILEDInvalid body.
409GEOGRAPHY_NAME_TAKENRenamed to a name already in use in the target district.
409GEOGRAPHY_RENAME_REFERENCEDRenaming or reparenting a municipality the school profile or a person's address already points at.

7.12 DELETE /api/lookups/municipalities/:publicId

Purpose: Delete an unused municipality.

Auth: Geography_DELETE.

Behavior: Permitted only when no school profile or person's address references it. There is no fourth child table — municipalities are the leaf of the hierarchy.

Errors:

HTTP StatusError CodeCondition
401AUTH_UNAUTHENTICATEDMissing/invalid JWT.
403PERMISSION_INSUFFICIENTActive role lacks Geography_DELETE.
404MUNICIPALITY_NOT_FOUNDNo municipality with that publicId.
409GEOGRAPHY_IN_USEThe school profile or a person's address still points at it.

8. Error Code Reference

Error CodeHTTP StatusMeaning
PROVINCE_NOT_FOUND404No province with the given publicId.
DISTRICT_NOT_FOUND404No district with the given publicId, or an unknown provinceId/districtId was supplied as a parent.
MUNICIPALITY_NOT_FOUND404No municipality with the given publicId.
GEOGRAPHY_NAME_TAKEN409The case-insensitive unique index on name (scoped to the parent for districts/municipalities) refused the write.
GEOGRAPHY_RENAME_REFERENCED409A rename or reparent was refused because a child row, the school profile, or a person's address already points at this row. Retire it instead.
GEOGRAPHY_IN_USE409A hard delete was refused because something still references this row.

Source: apps/api/src/common/types/error-codes.ts, around lines 198-206.

See Also

  • Backend doc: /docs/developer/geography/backend
  • Feature and flows doc: /docs/developer/geography/feature