Skip to content

SoarEarth/kotlin-api-example

Repository files navigation

SOAR Maps API Example

A production-ready Kotlin REST API for managing geographic map layers with PostGIS spatial queries, built with Micronaut framework.

🎯 Features

  • πŸ—ΊοΈ 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

πŸ“š Tech Stack

  • 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

πŸ“‹ Prerequisites

  • Java 21 or higher
  • Docker & Docker Compose (for database)
  • Gradle (included via wrapper)

πŸš€ Quick Start

1. Setup Environment

copy .env.example .env
REM Edit .env with your database credentials

2. Start Database

docker-compose up -d db

Wait for PostgreSQL to be ready. The init.sql script will automatically:

  • Enable PostGIS extension
  • Create map_layers table
  • Add spatial indexes
  • Insert sample data

3. Run Application

gradlew.bat run

The API will start at: http://localhost:8080

4. Test API

curl "http://localhost:8080/api/v1/map-layers/search?latitude=25.25&longitude=22.27"

πŸ“‘ API Endpoints

Search for Layers Containing a Point

GET /api/v1/map-layers/search?latitude=25.25&longitude=22.27

Query Parameters:

  • latitude (required): Latitude coordinate, range: -90 to 90
  • longitude (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 All Layers

GET /api/v1/map-layers

Response (200 OK):

[
  {
    "id": 1,
    "name": "North Africa Region",
    "geom": "POLYGON((20 20, 20 30, 30 30, 30 20, 20 20))"
  }
]

Get Layer by ID

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

Create New Layer

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))"
}

Error Responses

400 Bad Request:

{
  "status": 400,
  "error": "Bad Request",
  "message": "Latitude must be between -90 and 90",
  "path": "/api/v1/map-layers/search"
}

πŸ—οΈ Project Structure

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

πŸ›οΈ Architecture

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
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ§ͺ Testing

Run All Tests

gradlew.bat test

View Test Report

start build\reports\tests\test\index.html

Test Coverage

  • βœ… 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

Configuration is managed through environment variables with sensible defaults.

Environment Variables

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

application.yml

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: 10

🐳 Docker Deployment

Start Full Stack

docker-compose up -d

This starts:

  • PostgreSQL with PostGIS extension
  • Application container (port 8080)

Stop Services

docker-compose down

View Logs

docker-compose logs -f app

πŸ“Š Database Schema

CREATE 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

πŸ” Spatial Queries

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)
);

πŸ“ Logging

Logs are written to:

  • Console: Development debugging
  • File: logs/application.log with daily rotation

Log Levels

  • com.example β†’ INFO
  • io.micronaut β†’ INFO
  • io.micronaut.data β†’ DEBUG (shows SQL queries)
  • io.netty β†’ WARN

πŸ”§ Build Commands

# 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 distZip

πŸ› οΈ Development

Adding Dependencies

Edit build.gradle.kts:

dependencies {
    implementation("group:artifact:version")
}

Creating a New Endpoint

  1. Add method to MapLayerService
  2. Create DTO in MapLayerDto.kt (if needed)
  3. Add controller method in MapLayerController
  4. Write tests

βœ… Best Practices Implemented

  • βœ… 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

πŸ› Troubleshooting

Tests Fail with kotlin-reflect Error

Solution: The kotlin-reflect dependency is required:

implementation("org.jetbrains.kotlin:kotlin-reflect")

Database Connection Fails

Solution: Ensure PostgreSQL is running and credentials are correct:

docker-compose up -d db
docker-compose logs db

Port Already in Use

Solution: Change the port in .env or stop the conflicting service:

PORT=8081

πŸ“š Additional Resources

πŸ“„ License

This is an example project for demonstration purposes.

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Commit your changes
  4. Write/update tests
  5. Create a Pull Request

Built with ❀️ using Kotlin and Micronaut

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages