A production-ready Kotlin REST API for managing geographic map layers with PostGIS spatial queries, built with Micronaut framework.
- πΊοΈ Spatial Queries: Find map layers containing specific geographic points using PostGIS
- π RESTful API: Clean REST endpoints with proper HTTP semantics and versioning
- β Input Validation: Comprehensive validation at all layers (controller, service, repository)
- π Database Support: PostgreSQL with PostGIS extension for spatial operations
- π Security: Environment-based configuration, no hardcoded credentials
- π§ͺ Testing: Complete unit and integration test coverage
- π Logging: Structured logging with SLF4J and Logback
- π³ Docker Support: Multi-container deployment with Docker Compose
- ποΈ Clean Architecture: Layered design (Controller β Service β Repository)
- π DTOs: Separate data transfer objects from domain entities
- Language: Kotlin 2.0
- Framework: Micronaut 4.5
- Database: PostgreSQL 16 with PostGIS 3.4
- Build Tool: Gradle 8.x (Kotlin DSL)
- Testing: JUnit 5, Mockito-Kotlin
- Logging: Logback with file rotation
- Connection Pool: HikariCP
- Validation: Jakarta Bean Validation
- Java 21 or higher
- Docker & Docker Compose (for database)
- Gradle (included via wrapper)
copy .env.example .env
REM Edit .env with your database credentialsdocker-compose up -d dbWait for PostgreSQL to be ready. The init.sql script will automatically:
- Enable PostGIS extension
- Create
map_layerstable - Add spatial indexes
- Insert sample data
gradlew.bat runThe API will start at: http://localhost:8080
curl "http://localhost:8080/api/v1/map-layers/search?latitude=25.25&longitude=22.27"GET /api/v1/map-layers/search?latitude=25.25&longitude=22.27Query Parameters:
latitude(required): Latitude coordinate, range: -90 to 90longitude(required): Longitude coordinate, range: -180 to 180
Response (200 OK):
{
"query": {
"latitude": 25.25,
"longitude": 22.27
},
"results": [
{
"id": 1,
"name": "North Africa Region",
"geom": "POLYGON((20 20, 20 30, 30 30, 30 20, 20 20))"
}
],
"count": 1
}GET /api/v1/map-layersResponse (200 OK):
[
{
"id": 1,
"name": "North Africa Region",
"geom": "POLYGON((20 20, 20 30, 30 30, 30 20, 20 20))"
}
]GET /api/v1/map-layers/{id}Response (200 OK):
{
"id": 1,
"name": "North Africa Region",
"geom": "POLYGON((20 20, 20 30, 30 30, 30 20, 20 20))"
}Response (404 Not Found): When layer doesn't exist
POST /api/v1/map-layers
Content-Type: application/json
{
"name": "New Region",
"geom": "POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))"
}Response (201 Created):
{
"id": 4,
"name": "New Region",
"geom": "POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))"
}400 Bad Request:
{
"status": 400,
"error": "Bad Request",
"message": "Latitude must be between -90 and 90",
"path": "/api/v1/map-layers/search"
}src/
βββ main/
β βββ kotlin/com/example/
β β βββ Application.kt # Main application entry point
β β βββ MapLayer.kt # Domain entity
β β βββ MapLayerDto.kt # Data Transfer Objects
β β βββ MapLayerRepository.kt # Database access layer
β β βββ MapLayerService.kt # Business logic layer
β β βββ MapLayerController.kt # REST API controller
β β βββ ExceptionHandlers.kt # Global error handling
β βββ resources/
β βββ application.yml # Application configuration
β βββ logback.xml # Logging configuration
βββ test/
βββ kotlin/
β βββ MapLayerServiceTest.kt # Unit tests
β βββ MapLayerControllerTest.kt # Integration tests
βββ resources/
βββ application-test.yml # Test configuration
βββ logback-test.xml # Test logging
The application follows a clean, layered architecture:
βββββββββββββββββββββββββββββββββββββββ
β Controller Layer β β HTTP requests/responses
β - MapLayerController β Input validation
β - DTOs for API contracts β Error handling
ββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Service Layer β β Business logic
β - MapLayerService β Coordinate validation
β - Transaction boundaries β Logging
ββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Repository Layer β β Data access
β - MapLayerRepository β SQL queries
β - PostGIS spatial operations β CRUD operations
ββββββββββββββββ¬βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Database (PostgreSQL + PostGIS) β β Data persistence
β - map_layers table β Spatial indexes
βββββββββββββββββββββββββββββββββββββββ
gradlew.bat teststart build\reports\tests\test\index.html- β
Unit Tests (
MapLayerServiceTest): Business logic with mocked dependencies - β
Integration Tests (
MapLayerControllerTest): Full HTTP request/response cycle - β Validation Tests: Coordinate range validation
- β Error Handling Tests: Exception scenarios
Configuration is managed through environment variables with sensible defaults.
| Variable | Description | Default |
|---|---|---|
PORT |
Application port | 8080 |
JDBC_URL |
Database connection URL | jdbc:postgresql://localhost:5432/soar |
DB_USERNAME |
Database username | your_username |
DB_PASSWORD |
Database password | your_password |
micronaut:
application:
name: soar-maps-api-example
server:
port: ${PORT:8080}
cors:
enabled: true
datasources:
default:
url: ${JDBC_URL:`jdbc:postgresql://localhost:5432/soar`}
username: ${DB_USERNAME:`your_username`}
password: ${DB_PASSWORD:`your_password`}
maximum-pool-size: 10docker-compose up -dThis starts:
- PostgreSQL with PostGIS extension
- Application container (port 8080)
docker-compose downdocker-compose logs -f appCREATE TABLE map_layers (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
geom GEOMETRY(Polygon, 4326) NOT NULL
);
CREATE INDEX idx_map_layers_geom ON map_layers USING GIST (geom);- SRID 4326: WGS 84 coordinate system (standard lat/lon)
- GIST Index: Optimizes spatial queries
- Geometry Type: Polygon
The application uses PostGIS functions for spatial operations:
-- Find layers containing a point
SELECT id, name, ST_AsText(geom) as geom
FROM map_layers
WHERE ST_Contains(
geom,
ST_SetSRID(ST_MakePoint(:longitude, :latitude), 4326)
);Logs are written to:
- Console: Development debugging
- File:
logs/application.logwith daily rotation
com.exampleβ INFOio.micronautβ INFOio.micronaut.dataβ DEBUG (shows SQL queries)io.nettyβ WARN
# Clean build
gradlew.bat clean build
# Compile only
gradlew.bat compileKotlin
# Run application
gradlew.bat run
# Run tests
gradlew.bat test
# Generate distribution
gradlew.bat distZipEdit build.gradle.kts:
dependencies {
implementation("group:artifact:version")
}- Add method to
MapLayerService - Create DTO in
MapLayerDto.kt(if needed) - Add controller method in
MapLayerController - Write tests
- β Separation of Concerns: Clear layer boundaries
- β DTOs: API contracts separate from entities
- β Input Validation: Multiple validation layers
- β Error Handling: Global exception handlers
- β Logging: Structured with context
- β Testing: Unit and integration tests
- β Documentation: KDoc on all public APIs
- β Configuration: Environment-based
- β Security: No hardcoded secrets
- β Docker: Container-ready deployment
Solution: The kotlin-reflect dependency is required:
implementation("org.jetbrains.kotlin:kotlin-reflect")Solution: Ensure PostgreSQL is running and credentials are correct:
docker-compose up -d db
docker-compose logs dbSolution: Change the port in .env or stop the conflicting service:
PORT=8081
This is an example project for demonstration purposes.
- Fork the repository
- Create a feature branch
- Commit your changes
- Write/update tests
- Create a Pull Request
Built with β€οΈ using Kotlin and Micronaut