|
| 1 | +# http4k Example API |
| 2 | + |
| 3 | +A production-ready RESTful API built with http4k demonstrating clean architecture, JWT authentication, and OpenAPI documentation. |
| 4 | + |
| 5 | +It is hard to find a good example of a production-ready REST API built with Kotlin and http4k. This project aims to fill that gap. |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +- **JWT Authentication** - Secure endpoints with bearer token authentication |
| 10 | +- **OpenAPI 3.0** - Auto-generated API documentation from contract definitions |
| 11 | +- **Swagger UI** - Interactive API documentation interface |
| 12 | +- **Clean Architecture** - Domain-driven design with clear separation of concerns |
| 13 | +- **Functional Programming** - Uses Arrow-kt for functional error handling |
| 14 | +- **Comprehensive Testing** - Unit and integration tests with 121 test cases |
| 15 | +- **Code Quality** - ktlint integration for consistent code style |
| 16 | + |
| 17 | +## Tech Stack |
| 18 | + |
| 19 | +- **Framework**: [http4k](https://www.http4k.org/) - Lightweight, functional HTTP toolkit |
| 20 | +- **Language**: Kotlin 2.1.20 |
| 21 | +- **Runtime**: JVM 21 |
| 22 | +- **Security**: JWT (Auth0 java-jwt), BCrypt password hashing |
| 23 | +- **Serialization**: Kotlinx Serialization |
| 24 | +- **Testing**: Kotest, MockK, REST Assured |
| 25 | +- **Observability**: Micrometer, OpenTelemetry |
| 26 | +- **Code Quality**: ktlint |
| 27 | + |
| 28 | +## NOTE |
| 29 | + |
| 30 | +1. The authenticate endpoint should not be taken as best practice. The intent here is to show a flow of business logic across multiple components. |
| 31 | +2. This project uses unsafe CORS settings for demonstration only. Do not use this in production. |
| 32 | +3. This project has hard coded secrets in it. Do not use hard-coded secrets in production. |
| 33 | + |
| 34 | +## Features not implemented |
| 35 | + |
| 36 | +- Rate limiting |
| 37 | +- Metrics |
| 38 | +- Resilience strategies |
| 39 | + |
| 40 | +## Architecture |
| 41 | + |
| 42 | +The project follows **Clean Architecture** principles with clear separation between layers: |
| 43 | + |
| 44 | +``` |
| 45 | +src/main/kotlin/za/co/ee/learning/ |
| 46 | +├── domain/ # Domain layer - business logic |
| 47 | +│ ├── security/ # JWT and password providers |
| 48 | +│ ├── users/ # User domain entities |
| 49 | +│ │ └── usecases/ # Business use cases |
| 50 | +│ └── DomainError.kt # Domain error types |
| 51 | +├── infrastructure/ # Infrastructure layer |
| 52 | +│ ├── api/ # HTTP endpoints with contracts |
| 53 | +│ ├── database/ # Data persistence |
| 54 | +│ ├── routes/ # Route definitions |
| 55 | +│ └── security/ # Security implementations |
| 56 | +├── Application.kt # Main entry point |
| 57 | +├── Server.kt # Server configuration |
| 58 | +└── ServerConfig.kt # Configuration data class |
| 59 | +``` |
| 60 | + |
| 61 | +## Getting Started |
| 62 | + |
| 63 | +### Prerequisites |
| 64 | + |
| 65 | +- JDK 21 or later |
| 66 | +- Gradle (wrapper included) |
| 67 | + |
| 68 | +### Installation |
| 69 | + |
| 70 | +1. Clone the repository |
| 71 | +2. Navigate to the project directory: |
| 72 | + ```bash |
| 73 | + cd http4k-example |
| 74 | + ``` |
| 75 | + |
| 76 | +3. Build the project: |
| 77 | + ```bash |
| 78 | + ./gradlew build |
| 79 | + ``` |
| 80 | + |
| 81 | +### Running the Application |
| 82 | + |
| 83 | +Start the server: |
| 84 | +```bash |
| 85 | +./gradlew run |
| 86 | +``` |
| 87 | + |
| 88 | +The API will be available at `http://localhost:8080` |
| 89 | + |
| 90 | +## API Endpoints |
| 91 | + |
| 92 | +### Public Endpoints |
| 93 | + |
| 94 | +| Method | Endpoint | Description | |
| 95 | +|--------|----------|-------------| |
| 96 | +| GET | `/health` | Health check | |
| 97 | +| POST | `/api/v1/authenticate` | Authenticate user and get JWT token | |
| 98 | +| GET | `/swagger` | Swagger UI documentation | |
| 99 | +| GET | `/openapi.json` | OpenAPI 3.0 specification | |
| 100 | + |
| 101 | +### Protected Endpoints (Requires JWT) |
| 102 | + |
| 103 | +| Method | Endpoint | Description | |
| 104 | +|--------|----------|-------------| |
| 105 | +| GET | `/api/v1/users` | Get all users | |
| 106 | + |
| 107 | +### Authentication |
| 108 | + |
| 109 | +1. **Get a JWT token:** |
| 110 | + ```bash |
| 111 | + curl -X POST http://localhost:8080/api/v1/authenticate \ |
| 112 | + -H "Content-Type: application/json" \ |
| 113 | + -d '{"email":"[email protected]","password":"password123"}' |
| 114 | + ``` |
| 115 | + |
| 116 | +2. **Use the token:** |
| 117 | + ```bash |
| 118 | + curl http://localhost:8080/api/v1/users \ |
| 119 | + -H "Authorization: Bearer YOUR_JWT_TOKEN" |
| 120 | + ``` |
| 121 | + |
| 122 | +### Interactive Documentation |
| 123 | + |
| 124 | +Visit `http://localhost:8080/swagger` to explore and test the API using Swagger UI. |
| 125 | + |
| 126 | +## Testing |
| 127 | + |
| 128 | +### Run All Tests |
| 129 | +```bash |
| 130 | +./gradlew test |
| 131 | +``` |
| 132 | + |
| 133 | +### Run Specific Test Suites |
| 134 | +```bash |
| 135 | +# Unit tests only |
| 136 | +./gradlew test --tests "za.co.ee.learning.domain.*" |
| 137 | +./gradlew test --tests "za.co.ee.learning.infrastructure.api.*" |
| 138 | + |
| 139 | +# Integration tests only |
| 140 | +./gradlew test --tests "za.co.ee.learning.integration.*" |
| 141 | +``` |
| 142 | + |
| 143 | +### Test Coverage |
| 144 | + |
| 145 | +- **Unit Tests**: 77 tests covering domain logic, infrastructure, and API layers |
| 146 | +- **Integration Tests**: 44 tests covering full HTTP stack with authentication |
| 147 | +- **Total**: 121 tests |
| 148 | + |
| 149 | +## Code Quality |
| 150 | + |
| 151 | +### ktlint |
| 152 | + |
| 153 | +The project uses ktlint for maintaining consistent Kotlin code style. |
| 154 | + |
| 155 | +**Check code style:** |
| 156 | +```bash |
| 157 | +./gradlew ktlintCheck |
| 158 | +``` |
| 159 | + |
| 160 | +**Auto-format code:** |
| 161 | +```bash |
| 162 | +./gradlew ktlintFormat |
| 163 | +``` |
| 164 | + |
| 165 | +## Configuration |
| 166 | + |
| 167 | +The application can be configured via `ServerConfig` class: |
| 168 | + |
| 169 | +```kotlin |
| 170 | +ServerConfig( |
| 171 | + port = 8080, // Server port |
| 172 | + jwtSecret = "your-secret", // JWT signing secret |
| 173 | + jwtIssuer = "http4k", // JWT issuer |
| 174 | + jwtExpirationSeconds = 3600, // Token expiration time |
| 175 | + enableDebugFilters = true, // Enable request/response logging |
| 176 | + enableMetrics = true // Enable Micrometer/OpenTelemetry |
| 177 | +) |
| 178 | +``` |
| 179 | + |
| 180 | +## Project Highlights |
| 181 | + |
| 182 | +### Contract-First API Design |
| 183 | + |
| 184 | +Endpoints are defined using http4k's contract DSL, which automatically generates OpenAPI documentation: |
| 185 | + |
| 186 | +```kotlin |
| 187 | +val route: ContractRoute = "/api/v1/users" meta { |
| 188 | + summary = "Get Users" |
| 189 | + description = "Retrieves list of all users (requires JWT authentication)" |
| 190 | + security = BearerAuthSecurity("JWT Bearer Token") |
| 191 | + returning(Status.OK, usersResponseLens to listOf(...)) |
| 192 | +} bindContract Method.GET to handler |
| 193 | +``` |
| 194 | + |
| 195 | +### Functional Error Handling |
| 196 | + |
| 197 | +Uses Arrow-kt's `Either` type for explicit error handling: |
| 198 | + |
| 199 | +```kotlin |
| 200 | +fun authenticate(request: AuthenticateRequest): DomainResult<AuthenticateResponse> = |
| 201 | + either { |
| 202 | + val user = userRepository.findByEmail(request.email) |
| 203 | + .bind() |
| 204 | + .toEither { DomainError.NotFound("User not found") } |
| 205 | + .bind() |
| 206 | + // ... continue with business logic |
| 207 | + } |
| 208 | +``` |
| 209 | + |
| 210 | +### JWT Security Filter |
| 211 | + |
| 212 | +Conditional JWT authentication with path exclusions: |
| 213 | + |
| 214 | +```kotlin |
| 215 | +JWTFilter( |
| 216 | + jwtProvider = jwtProvider, |
| 217 | + excludedPaths = setOf("/health", "/api/v1/authenticate", "/openapi.json") |
| 218 | +) |
| 219 | +``` |
| 220 | + |
| 221 | +## Development |
| 222 | + |
| 223 | +### Project Structure |
| 224 | + |
| 225 | +- `domain/` - Pure business logic, no framework dependencies |
| 226 | +- `infrastructure/api/` - HTTP endpoints with contract definitions |
| 227 | +- `infrastructure/database/` - Data access implementations |
| 228 | +- `infrastructure/routes/` - Route composition and OpenAPI generation |
| 229 | +- `infrastructure/security/` - Security implementations (JWT, BCrypt, filters) |
| 230 | + |
| 231 | +### Adding a New Endpoint |
| 232 | + |
| 233 | +1. Create endpoint class in `infrastructure/api/` |
| 234 | +2. Define contract with metadata |
| 235 | +3. Add route to `ContractRoutes` |
| 236 | +4. Update JWT filter exclusions if public |
| 237 | + |
| 238 | +## License |
| 239 | + |
| 240 | +This is an example project for learning purposes. |
| 241 | + |
| 242 | +## Resources |
| 243 | + |
| 244 | +- [http4k Documentation](https://www.http4k.org/) |
| 245 | +- [Arrow-kt Documentation](https://arrow-kt.io/) |
| 246 | +- [Kotest Documentation](https://kotest.io/) |
0 commit comments