📍 Location Service
The Location Service provides a complete REST API for managing hierarchical locations with parent-child relationships and ancestor tracking. It's designed to handle organizational structures, geographic hierarchies, and location-based data using the Nodeblocks functional composition approach and MongoDB integration.
🚀 Quickstart
import express from 'express';
import {middlewares, services, drivers} from '@nodeblocks/backend-sdk';
const {nodeBlocksErrorMiddleware} = middlewares;
const {locationService} = services;
const {withMongo} = drivers;
const connectToDatabase = withMongo('mongodb://localhost:27017/?authSource=admin', 'dev', 'user', 'password');
express()
.use(
locationService(
{
...(await connectToDatabase('locations')),
...(await connectToDatabase('identities')),
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
authMode: 'bearer', // or 'cookie',
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
},
),
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));
📋 Endpoint Summary
Location Operations
| Method | Path | Description | Auth Required |
|---|---|---|---|
POST | /locations | Create a new location | ✅ Admin |
GET | /locations/:locationId | Retrieve a location by ID | ❌ |
GET | /locations | List all locations | ❌ |
PATCH | /locations/:locationId | Update a location | ✅ Admin |
DELETE | /locations/:locationId | Delete a location | ✅ Admin |
🗄️ Entity Schema
The location entity combines base fields (auto-generated) with location-specific data:
{
"name": "string",
"code": "string",
"type": "string",
"parentId": "string",
"ancestors": ["string"],
"createdAt": "string (datetime)",
"id": "string",
"updatedAt": "string (datetime)"
}
Field Details
| Field | Type | Auto-Generated | Required | Description |
|---|---|---|---|---|
name | string | ❌ | ✅ | Location name |
code | string | ❌ | ✅ | Unique location identifier code |
type | string | ❌ | ✅ | Free-form location type (for example, ORGANIZATION, REGION, CITY, or BUILDING) |
parentId | string | ❌ | ❌ | Parent location ID for hierarchical structure |
ancestors | string[] | ✅ | ✅ | Array of ancestor location IDs (auto-calculated) |
createdAt | datetime | ✅ | ✅ | Creation timestamp |
id | string | ✅ | ✅ | Unique identifier (UUID) |
updatedAt | datetime | ✅ | ✅ | Last modification timestamp |
📝 Note: Auto-generated fields are set by the service and should not be included in create/update requests. The
parentIdfield enables hierarchical location structures, andancestorsis automatically calculated from the parent hierarchy.
🔐 Authentication Headers
For protected endpoints, use an access token in the Authorization header when authMode is 'bearer' and read the token from cookies when authMode is 'cookie':
Authorization: Bearer <admin_access_token>
x-nb-fingerprint: <device_fingerprint>
⚠️ Important: The
x-nb-fingerprintheader is required for all authenticated requests if fingerprint was specified during authorization. Without it, requests will return 401 Unauthorized.
🔧 API Endpoints
1. Create Location
Creates a new location with the provided information and optional hierarchical parent relationship.
Request:
- Method:
POST - Path:
/locations - Headers:
Content-Type: application/jsonAuthorization: Bearer <token>x-nb-fingerprint: <device-fingerprint>
- Authorization: Access token required (
bearerby default,cookiewhenauthMode: 'cookie'); admin only
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Location name |
code | string | ✅ | Unique location identifier code |
type | string | ✅ | Location type classification |
parentId | string | ❌ | Parent location ID for hierarchical structure |
Response Body:
| Field | Type | Description |
|---|---|---|
id | string | Unique location identifier |
name | string | Location name |
code | string | Unique location identifier code |
type | string | Location type classification |
parentId | string | Parent location ID (if applicable) |
ancestors | string[] | Array of ancestor location IDs |
createdAt | string | Creation timestamp |
updatedAt | string | Last update timestamp |
Validation:
- Schema Validation: Enforced automatically (name, code, type required)
- Route Validators:
- Require authenticated request (access token)
- Require admin role
Example Request:
curl -X POST http://localhost:8089/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>" \
-d '{
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-123"
}'
Success Response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "location-uuid",
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-123",
"ancestors": ["org-456", "region-789", "city-123"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
2. List Locations
Retrieves a paginated list of locations with public access and pagination support.
Request:
- Method:
GET - Path:
/locations - Authorization: None required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page | number | ❌ | Page number for pagination (1-1000) |
limit | number | ❌ | Number of items per page (1-50) |
Response Body: Paginated response with locations array and metadata.
Response Structure:
{
"data": [
{
"id": "string",
"name": "string",
"code": "string",
"type": "string",
"parentId": "string",
"ancestors": ["string"],
"createdAt": "string",
"updatedAt": "string"
}
],
"metadata": {
"pagination": {
"page": number,
"limit": number,
"total": number,
"totalPages": number,
"hasNext": boolean,
"hasPrev": boolean
}
}
}
Validation:
- Schema Validation: Query parameter validation for pagination (page, limit)
- Route Validators: None
Example Request:
curl "http://localhost:8089/locations?page=1&limit=10"
Success Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{
"id": "location-uuid",
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-123",
"ancestors": ["org-456", "region-789", "city-123"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"metadata": {
"pagination": {
"page": 1,
"limit": 10,
"total": 25,
"totalPages": 3,
"hasNext": true,
"hasPrev": false
}
}
}
3. Get Location by ID
Retrieves a location by ID with public access.
Request:
- Method:
GET - Path:
/locations/:locationId - Authorization: None required
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
locationId | string | ✅ | Unique location identifier |
Response Body:
| Field | Type | Description |
|---|---|---|
id | string | Unique location identifier |
name | string | Location name |
code | string | Unique location identifier code |
type | string | Location type classification |
parentId | string | Parent location ID (if applicable) |
ancestors | string[] | Array of ancestor location IDs |
createdAt | string | Creation timestamp |
updatedAt | string | Last update timestamp |
Validation:
- Schema Validation: Path parameter validation for locationId
- Route Validators: None
Example Request:
curl http://localhost:8089/locations/loc-123
Success Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "loc-123",
"name": "San Francisco",
"code": "SF",
"type": "CITY",
"parentId": "region-456",
"ancestors": ["org-789", "region-456"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
4. Update Location
Updates an existing location with partial field modifications and admin authentication.
Request:
- Method:
PATCH - Path:
/locations/:locationId - Headers:
Content-Type: application/json - Authorization: Access token required (
bearerby default,cookiewhenauthMode: 'cookie'); admin only
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
locationId | string | ✅ | Unique location identifier |
Request Body (all fields optional):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ❌ | Location name |
code | string | ❌ | Unique location identifier code |
type | string | ❌ | Location type classification |
Response Body:
| Field | Type | Description |
|---|---|---|
id | string | Unique location identifier |
name | string | Updated location name |
code | string | Updated location identifier code |
type | string | Updated location type |
parentId | string | Parent location ID (unchanged) |
ancestors | string[] | Array of ancestor location IDs (unchanged) |
createdAt | string | Creation timestamp |
updatedAt | string | Last update timestamp |
Validation:
- Schema Validation: Enforced automatically (partial updates, limited fields allowed)
- Route Validators:
- Require authenticated request (access token)
- Require admin role
Example Request:
curl -X PATCH http://localhost:8089/locations/loc-123 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>" \
-d '{
"name": "Updated Headquarters Name",
"code": "HQ-UPDATED"
}'
Success Response:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "loc-123",
"name": "Updated Headquarters Name",
"code": "HQ-UPDATED",
"type": "BUILDING",
"parentId": "region-456",
"ancestors": ["org-789", "region-456"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:20:00Z"
}
5. Delete Location
Deletes an existing location with comprehensive hierarchy validation to prevent orphaned child locations.
Request:
- Method:
DELETE - Path:
/locations/:locationId - Authorization: Access token required (
bearerby default,cookiewhenauthMode: 'cookie'); admin only
URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
locationId | string | ✅ | Unique location identifier |
Response Body:
| Field | Type | Description |
|---|---|---|
| No response body | - | Delete endpoint returns no response body on success |
Validation:
- Schema Validation: Path parameter validation for locationId
- Route Validators:
- Require authenticated request (access token)
- Require admin role
- Validates no descendant locations exist (prevents deletion if children exist)
Example Request:
curl -X DELETE http://localhost:8089/locations/loc-123 \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>"
Success Response:
HTTP/1.1 204 No Content
Error Responses:
When location has descendant locations:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"message": "Dependent child locations still exist"
}
}
When delete fails because the location could not be removed:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": {
"message": "Location could not be deleted"
}
}
Location Types
The SDK treats
typeas a free-form string — the table below lists suggested conventions, not an enum enforced by the schema.
The service commonly uses these location type classifications:
| Type | Description | Example |
|---|---|---|
ORGANIZATION | Root-level organizational entities | Global Corp, Acme Inc |
REGION | Geographic or administrative regions | West Coast, EMEA, APAC |
CITY | Municipal or urban areas | New York, Tokyo, London |
BUILDING | Physical structures or facilities | HQ Building, Branch Office |
Hierarchical Relationships
Parent-Child Relationships
Locations can have parent-child relationships:
// Create organization (no parent)
const org = await createLocation({
name: 'Tech Corp',
code: 'TECH',
type: 'ORGANIZATION',
});
// Create region under organization
const region = await createLocation({
name: 'West Coast',
code: 'WEST',
type: 'REGION',
parentId: org.id,
});
// Create city under region
const city = await createLocation({
name: 'San Francisco',
code: 'SF',
type: 'CITY',
parentId: region.id,
});
Ancestor Chains
Each location maintains a complete ancestor chain for efficient hierarchical queries:
{
"id": "building-uuid",
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-uuid",
"ancestors": ["org-uuid", "region-uuid", "city-uuid"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
Authorization & Security
Access Control
Location endpoints use different authorization rules:
| Method | Path | Auth |
|---|---|---|
GET | /locations | Public (no auth) |
GET | /locations/:locationId | Public (no auth) |
POST | /locations | Authenticated admin |
PATCH | /locations/:locationId | Authenticated admin |
DELETE | /locations/:locationId | Authenticated admin |
// Service configuration with admin identity type (required for write operations)
const config = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
authMode: 'bearer', // or 'cookie'
identity: {
typeIds: {
admin: '100', // Admin users
guest: '000', // Guest users
regular: '001', // Regular users
},
},
};
⚙️ Configuration Options
Service Configuration
interface LocationServiceConfiguration {
authSecrets: {
authEncSecret: string; // JWT encryption secret
authSignSecret: string; // JWT signing secret
};
authMode?: 'bearer' | 'cookie'; // Default: bearer behavior when unset
identity?: {
typeIds?: {
admin: string;
guest: string;
regular: string;
};
};
}
🍪 Cookie auth: When
authMode: 'cookie', protected routes read the access token from cookies. Host apps must registercookie-parser.
Datastore
| Collection | Required | Description |
|---|---|---|
identities | ✅ | Identity lookups / auth context |
locations | ✅ | Location documents |
Usage Examples
Basic Setup
import express from 'express';
import {services, drivers} from '@nodeblocks/backend-sdk';
const {locationService} = services;
const {withMongo} = drivers;
const app = express();
// Database setup
const connectToDatabase = withMongo('mongodb://localhost:27017/?authSource=admin', 'dev', 'user', 'password');
// Service configuration
const locationServiceConfig = {
authSecrets: {
authEncSecret: process.env.AUTH_ENC_SECRET,
authSignSecret: process.env.AUTH_SIGN_SECRET,
},
authMode: 'bearer', // or 'cookie'
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
};
// Data stores
const dataStores = {
...(await connectToDatabase('identities')),
...(await connectToDatabase('locations')),
};
// Mount service
app.use('/api/locations', locationService(dataStores, locationServiceConfig));
Creating Location Hierarchies
// 1. Create root organization
const orgResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
Authorization: `Bearer ${adminToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Global Enterprises',
code: 'GLOBAL',
type: 'ORGANIZATION',
}),
});
// 2. Create regional division
const regionResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
Authorization: `Bearer ${adminToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'North America',
code: 'NA',
type: 'REGION',
parentId: orgResponse.body.id,
}),
});
// 3. Create city location
const cityResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
Authorization: `Bearer ${adminToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'New York',
code: 'NYC',
type: 'CITY',
parentId: regionResponse.body.id,
}),
});
// 4. Create building
const buildingResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
Authorization: `Bearer ${adminToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Manhattan Office',
code: 'MANHATTAN',
type: 'BUILDING',
parentId: cityResponse.body.id,
}),
});
console.log('Created hierarchy:');
console.log('Organization -> Region -> City -> Building');
Service Architecture
Feature Composition
The location service uses feature composition for modularity:
export const locationService: LocationService = (dataStores, configuration) => {
return defService(
partial(
compose(
createLocationFeature,
getLocationFeature,
updateLocationFeature,
deleteLocationFeature,
findLocationsFeature,
),
[
{
authenticate: configuration.authMode === 'cookie' ? getCookieTokenInfo : getBearerTokenInfo,
configuration,
dataStores,
},
],
),
);
};
Middleware Integration
The service integrates with standard Nodeblocks middleware:
- Authentication: Bearer token validation
- Authorization: Identity type checking
- Error Handling: Structured error responses
- Request Logging: Comprehensive request tracking
Database Considerations
Recommended Indexes
For optimal performance, create these indexes on the locations collection:
// Unique index on location ID
db.locations.createIndex({id: 1}, {unique: true});
// Index for parent-child queries
db.locations.createIndex({parentId: 1});
// Index for ancestor chain queries
db.locations.createIndex({ancestors: 1});
// Compound index for hierarchical queries
db.locations.createIndex({type: 1, ancestors: 1});
// Index for code lookups
db.locations.createIndex({code: 1}, {unique: true});
Data Consistency
The service maintains consistency through:
- Referential Integrity: Parent validation before creation
- Ancestor Accuracy: Automatic ancestor chain calculation
- Code Uniqueness: Recommended unique index on
codefor lookup enforcement
Error Handling
Common Error Responses
Validation Errors (400)
{
"error": {
"data": [
"request body must have required property 'name'",
"request body must have required property 'code'",
"request body must have required property 'type'"
],
"message": "Validation Error"
}
}
Authorization Errors (403)
{
"error": {
"message": "Identity is not authorized to access this resource"
}
}
Not Found Errors (404)
{
"error": {
"message": "Location not found"
}
}
Database Errors (500)
{
"error": {
"message": "Failed to create location"
}
}
Future Enhancements
- Location Retrieval: GET endpoints for location queries
- Location Updates: PATCH endpoints for location modifications
- Location Deletion: DELETE endpoints with cascade options
- Bulk Operations: Bulk creation, update, and deletion
- Advanced Queries: Filtering by type, hierarchy, and custom criteria
- Geographic Features: Coordinate support and geographic queries
- Import/Export: CSV and JSON import/export capabilities
Extended Authorization
- Role-Based Access: Location-specific permissions
- Organization Scoping: Organization-based location access
- Hierarchical Permissions: Permission inheritance through location trees
Migration & Setup
Database Setup
// Create locations collection
db.createCollection('locations');
// Create indexes
db.locations.createIndex({id: 1}, {unique: true});
db.locations.createIndex({parentId: 1});
db.locations.createIndex({ancestors: 1});
db.locations.createIndex({type: 1, ancestors: 1});
db.locations.createIndex({code: 1}, {unique: true});
Application Integration
// Add to main application
import {services} from '@nodeblocks/backend-sdk';
const {locationService} = services;
// Configure and mount
const locationConfig = {
/* configuration */
};
const locationDataStores = {
/* data stores */
};
app.use('/api/locations', locationService(locationDataStores, locationConfig));
Testing
Service Testing
import {services} from '@nodeblocks/backend-sdk';
const {locationService} = services;
import request from 'supertest';
// Test service integration
describe('Location Service', () => {
it('Should create locations with proper hierarchy', async () => {
const app = express();
app.use('/locations', locationService(dataStores, config));
const response = await request(app).post('/locations').set('Authorization', `Bearer ${adminToken}`).send({
name: 'Test Location',
code: 'TEST',
type: 'BUILDING',
});
expect(response.status).toBe(201);
expect(response.body.ancestors).toEqual([]);
});
});
The Location Service provides a solid foundation for hierarchical location management with room for future expansion and enhanced functionality.