diff --git a/.env b/.env
deleted file mode 100644
index eeaf5c3..0000000
--- a/.env
+++ /dev/null
@@ -1 +0,0 @@
-JWT_SIGNING_SECRET=supersecret
\ No newline at end of file
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..bf51fbb
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+JWT_SECRET=default-secret-key
+TLS_CA_PATH=
+TLS_CERT_PATH=
+TLS_KEY_PATH=
+TLS_REQUIRE_AUTH=false
+
+ENABLE_DOCS=true
+PORT=8080
+TLS_PORT=8443
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..73f290f
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,23 @@
+name: Lint
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ lint:
+ name: Run on Ubuntu
+ runs-on: ubuntu-latest
+ steps:
+ - name: Clone the code
+ uses: actions/checkout@v4
+
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '~1.25'
+
+ - name: Run linter
+ uses: golangci/golangci-lint-action@v7
+ with:
+ version: v2.8.0
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b8495c1
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,20 @@
+name: Test
+
+on:
+ push:
+ pull_request:
+ branches:
+ - main
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: '1.25.5'
+
+ - name: Test
+ run: go test -v ./...
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f4a42ce..b4a30a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
public
.DS_Store
/.history
-Makefile
\ No newline at end of file
+Makefile
+certs
+.env
+coverage.txt
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..2058ecb
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,47 @@
+version: "2"
+run:
+ allow-parallel-runners: true
+linters:
+ default: none
+ enable:
+ - copyloopvar
+ - dupl
+ - errcheck
+ - ginkgolinter
+ - goconst
+ - gocyclo
+ - govet
+ - ineffassign
+ - lll
+ - misspell
+ - nakedret
+ - prealloc
+ - revive
+ - staticcheck
+ - unconvert
+ - unparam
+ - unused
+ settings:
+ revive:
+ rules:
+ - name: comment-spacings
+ exclusions:
+ generated: lax
+ rules:
+ - linters:
+ - dupl
+ - lll
+ path: /*
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
+formatters:
+ enable:
+ - gofmt
+ exclusions:
+ generated: lax
+ paths:
+ - third_party$
+ - builtin$
+ - examples$
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 13566b8..0000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml
-# Editor-based HTTP Client requests
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 32aa743..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/okapi-example.iml b/.idea/okapi-example.iml
deleted file mode 100644
index 5e764c4..0000000
--- a/.idea/okapi-example.iml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 275ce81..95c2df4 100644
--- a/README.md
+++ b/README.md
@@ -1,82 +1,167 @@
# Okapi Example
-A simple example demonstrating the Okapi API Framework
+A comprehensive example application showcasing the Go Okapi API Framework's core features and best practices.
-Okapi is a modern, minimalist HTTP web framework for Go, inspired by FastAPI's elegance. Designed for simplicity, performance, and developer happiness, it helps you build fast, scalable, and well-documented APIs with minimal boilerplate.
+Okapi is a modern, minimalist HTTP web framework for Go, inspired by FastAPI's elegance. Built for simplicity, performance, and developer happiness, it enables you to build fast, scalable, and well-documented APIs with minimal boilerplate.
-* [Okapi](https://github.com/jkaninda/okapi)
-* [Source Code](https://github.com/jkaninda/okapi-example)
-* [Docker Hub](https://hub.docker.com/r/jkaninda/okapi-example)
+## 🔗 Links
-## Prerequisites
+- **Framework**: [Okapi on GitHub](https://github.com/jkaninda/okapi)
+- **Source Code**: [okapi-example](https://github.com/jkaninda/okapi-example)
+- **Docker**: [jkaninda/okapi-example](https://hub.docker.com/r/jkaninda/okapi-example)
-- Go installed
-- Git installed
+## ✨ What's Included
-## Features
+This example demonstrates:
-- Basic Okapi implementation example
-- Okapi middlewares
-- Okapi Route Definition
-- Ready-to-run code structure
+- **Core Framework Usage** - Basic Okapi implementation patterns
+- **Middleware Integration** - Custom and built-in middleware examples
+- **Route Organization** - Structured route definitions and grouping
+- **Real-time Communication** - Server-Sent Events (SSE) and WebSocket implementations
+- **Template Rendering** - HTML template integration
+- **API Documentation** - Automatic Swagger/OpenAPI generation
+- **Production-Ready Structure** - Clean, maintainable code organization
-## Getting Started
+## 🚀 Quick Start
-### Clone the Repository
+### Prerequisites
+
+- Go 1.21 or higher
+- Git
+
+### Local Development
```shell
+# Clone the repository
git clone https://github.com/jkaninda/okapi-example
cd okapi-example
-```
-
-### Install Dependencies
-```shell
+# Install dependencies
go mod tidy
-```
-### Run the Application
-
-```shell
+# Run the application
go run .
```
-### Using Docker
+The server will start at `http://localhost:8080`
+
+### Docker Deployment
```shell
-docker run --rm --name okapi-example -p 8080:8080 jkaninda/okapi-example
+docker run --rm --name okapi-example \
+ -p 8080:8080 \
+ -e JWT_SIGNING_SECRET=your-secret-key \
+ jkaninda/okapi-example
+```
+
+### With HTTPS/TLS:
+
+```sh
+docker run --rm --name okapi-example \
+ -p 8080:8080 \
+ -p 8443:8443 \
+ -e JWT_SECRET=your-secret-key \
+ -e TLS_CERT_PATH=/certs/server.crt \
+ -e TLS_KEY_PATH=/certs/server.key \
+ -v /path/to/certs:/certs:ro \
+ jkaninda/okapi-example
```
-Use `JWT_SIGNING_SECRET` environment variable if you want to change JWT secret, default: `supersecret`
-Visit [`http://localhost:8080`](http://localhost:8080) to see the response:
+## ⚙️ Configuration
+
+Configure the application using environment variables:
+
+### Security
+
+| Variable | Description | Default |
+| ------------ | ------------------------ | -------------------- |
+| `JWT_SECRET` | JWT token signing secret | `default-secret-key` |
+
+### TLS/HTTPS
-```json
-{"message": "Welcome to the Okapi Web Framework!"}
+| Variable | Description | Default |
+| ------------------ | ----------------------------------------- | -------------------------- |
+| `TLS_CERT_PATH` | Path to TLS certificate file | _(empty - TLS disabled)_ |
+| `TLS_KEY_PATH` | Path to TLS private key file | _(empty - TLS disabled)_ |
+| `TLS_CA_PATH` | Path to CA certificate for mutual TLS | _(empty - no client auth)_ |
+| `TLS_REQUIRE_AUTH` | Require client certificate authentication | `false` |
+
+### Server
+
+| Variable | Description | Default |
+| ------------- | ------------------------------- | ------- |
+| `PORT` | HTTP server port | `8080` |
+| `TLS_PORT` | HTTPS server port | `8443` |
+| `ENABLE_DOCS` | Enable Swagger UI documentation | `true` |
+
+### Example Configuration
+
+Create a `.env` file in the project root:
+
+```dotenv
+# Security
+JWT_SECRET=your-secret-key
+
+# TLS Configuration (optional)
+#TLS_CERT_PATH=/path/to/cert.pem
+#TLS_KEY_PATH=/path/to/key.pem
+#TLS_CA_PATH=/path/to/ca.pem
+#TLS_REQUIRE_AUTH=false
+
+# Server Configuration
+ENABLE_DOCS=true
+PORT=8080
+TLS_PORT=8443
```
-Visit [`http://localhost:8080/docs/`](http://localhost:8080/docs/) to see the documentation
+## 📚 Explore the Example
+
+Once running, visit:
-## Project Structure
+- **Home Page**: [http://localhost:8080](http://localhost:8080) - Welcome page with example overview
+- **API Documentation**: [http://localhost:8080/docs/](http://localhost:8080/docs/) - Interactive Swagger UI
+
+## 📁 Project Structure
```
-.
-├── main.go # Main application file
-├── middlewares # Middlewares package
-├── controllers # Controllers package
-├── routes # Routes package
-├── models # Models package
-└── README.md # Project documentation
+okapi-example/
+├── main.go # Application entry point and server setup
+├── routes/ # API route definitions and handlers
+├── middlewares/ # Custom middleware implementations
+├── services/ # Business logic and service layer
+├── models/ # Data models and structures
+├── session/ # Session management utilities
+└── README.md
```
-### Swagger UI Preview
+### Architecture Highlights
+
+- **Layered Design**: Clear separation between routes, services, and models
+- **Middleware Pipeline**: Reusable middleware for authentication, logging, and CORS
+- **Service Layer**: Business logic and service layer
+- **Session Management**: Built-in session handling utilities
+
+## 📸 Screenshots
+
+### Home Page
+
+
+
+### Interactive API Documentation
+
+Okapi automatically generates comprehensive Swagger UI documentation for all your routes, making API exploration and testing effortless.
-Okapi automatically generates Swagger UI for all routes:
+
+## 🎯 Next Steps
-
+After exploring this example:
----
-## License
+1. **Read the Documentation**: Visit the [Okapi docs](https://github.com/jkaninda/okapi) for detailed guides
+2. **Customize Routes**: Modify the routes in the `routes/` directory to fit your needs
+3. **Add Features**: Extend the example with database integration, authentication, etc.
+4. **Deploy**: Use the included Docker setup for production deployments
-[MIT](LICENSE) - Feel free to use and modify this example.
+## 📄 License
+[MIT](LICENSE) - Feel free to use, modify, and distribute this example for any purpose.
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..02427da
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,159 @@
+package config
+
+import (
+ "fmt"
+ "strings"
+
+ goutils "github.com/jkaninda/go-utils"
+ "github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi-example/session"
+ "github.com/jkaninda/okapi-example/utils"
+ "github.com/joho/godotenv"
+)
+
+type Config struct {
+ Database DatabaseConfig
+ Redis RedisConfig
+ Server ServerConfig
+ Cors CorsConfig
+ JWT JWTConfig
+ Log LogConfig
+ SessionManager *session.SessionManager
+}
+
+type DatabaseConfig struct {
+ dbHost string
+ dbUser string
+ dbPassword string
+ dbName string
+ dbPort int
+ dbSslMode string
+}
+
+type RedisConfig struct {
+ URL string
+}
+
+type ServerConfig struct {
+ port int
+ tlsPort int
+ environment string
+ enableDocs bool
+ tls Tls
+}
+type Tls struct {
+ Cert string
+ Key string
+ CA string
+ RequireAuth bool
+}
+type CorsConfig struct {
+ AllowedOrigins []string
+}
+
+type JWTConfig struct {
+ Secret string
+}
+
+type LogConfig struct {
+ Level string
+}
+
+func New() *Config {
+ // Load .env file if it exists
+ _ = godotenv.Load()
+ return &Config{
+ Database: DatabaseConfig{
+ dbHost: goutils.Env("DB_HOST", "localhost"),
+ dbUser: goutils.Env("DB_USER", ""),
+ dbPassword: goutils.Env("DB_PASSWORD", ""),
+ dbName: goutils.Env("DB_NAME", "account"),
+ dbPort: goutils.EnvInt("DB_PORT", 5432),
+ dbSslMode: goutils.Env("DB_SSL_MODE", "disable"),
+ },
+ Redis: RedisConfig{
+ URL: goutils.Env("REDIS_URL", "redis://localhost:6379/0"),
+ },
+ Server: ServerConfig{
+ enableDocs: goutils.EnvBool("ENABLE_DOCS", true),
+ port: goutils.EnvInt("PORT", 8080),
+ tlsPort: goutils.EnvInt("TLS_PORT", 8443),
+ environment: goutils.Env("ENVIRONMENT", "development"),
+ tls: Tls{
+ Cert: goutils.Env("TLS_CERT_PATH", ""),
+ Key: goutils.Env("TLS_KEY_PATH", ""),
+ // mTLS
+ CA: goutils.Env("TLS_CA_PATH", ""),
+ RequireAuth: goutils.EnvBool("TLS_REQUIRE_AUTH", false),
+ },
+ },
+
+ Cors: CorsConfig{
+ AllowedOrigins: strings.Split(goutils.Env("CORS_ALLOWED_ORIGINS", "http://localhost:8080"), ","),
+ },
+ JWT: JWTConfig{
+ Secret: goutils.Env("JWT_SECRET", "default-secret-key"),
+ },
+ Log: LogConfig{
+ Level: goutils.Env("LOG_LEVEL", "info"),
+ },
+ SessionManager: session.New(),
+ }
+}
+func (c *Config) validate() error {
+ if c.Redis.URL == "" {
+ return fmt.Errorf("REDIS_URL is required")
+ }
+ if c.Server.port == 0 {
+ return fmt.Errorf("PORT is required")
+ }
+ return nil
+}
+func (c *Config) Initialize(app *okapi.Okapi) error {
+ if err := c.validate(); err != nil {
+ return err
+ }
+
+ // Init Doc
+ if c.Server.enableDocs {
+ app.WithOpenAPIDocs(okapi.OpenAPI{
+ Title: utils.AppName,
+ Version: utils.AppVersion,
+ License: okapi.License{
+ Name: "MIT",
+ },
+ Contact: okapi.Contact{
+ Name: "Jonas Kaninda",
+ Email: "me@jkaninda.dev",
+ URL: "https://github.com/jkaninda/okapi"},
+ SecuritySchemes: okapi.SecuritySchemes{
+ {
+ Name: "bearerAuth",
+ Type: "http",
+ Scheme: "bearer",
+ BearerFormat: "JWT",
+ },
+ },
+ })
+ }
+ if len(c.Cors.AllowedOrigins) > 0 {
+ app.WithCORS(okapi.Cors{AllowedOrigins: c.Cors.AllowedOrigins})
+ }
+ // Init TLS server
+ if len(c.Server.tls.Cert) > 0 && len(c.Server.tls.Key) > 0 {
+ tls, err := goutils.LoadTLSConfig(c.Server.tls.Cert, c.Server.tls.Key, c.Server.tls.CA, c.Server.tls.RequireAuth)
+ if err != nil {
+ panic(err)
+ } else {
+ tlsAdd := fmt.Sprintf(":%d", c.Server.tlsPort)
+ // Add tls server
+ app.With(okapi.WithTLSServer(tlsAdd, tls))
+ // Or use a single server
+ // app.With(okapi.WithTLS(tls))
+ }
+ }
+
+ addr := fmt.Sprintf(":%d", c.Server.port)
+ app.With(okapi.WithAddr(addr))
+ return nil
+}
diff --git a/controllers/controller.go b/controllers/controller.go
deleted file mode 100644
index 98d1214..0000000
--- a/controllers/controller.go
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2025 Jonas Kaninda
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package controllers
-
-import (
- "encoding/json"
- "fmt"
- "github.com/jkaninda/logger"
- "github.com/jkaninda/okapi"
- "github.com/jkaninda/okapi-example/middlewares"
- "github.com/jkaninda/okapi-example/models"
- "net/http"
- "os"
- "strconv"
-)
-
-type BookController struct{}
-type HomeController struct{}
-type AuthController struct{}
-
-var (
- books = []*models.Book{
- {Id: 1, Title: "Book One", Price: 100},
- {Id: 2, Title: "Book Two", Price: 200},
- {Id: 3, Title: "Book Three", Price: 300},
- }
-)
-
-// ****************** Controllers *****************
-
-func (hc *HomeController) Home(c okapi.Context) error {
- return c.OK(okapi.M{"message": "Welcome to the Okapi Web Framework!"})
-}
-
-func (hc *HomeController) WhoAmI(c okapi.Context) error {
- email := c.Header("current_user_email")
- if email == "" {
- logger.Warn("no email found")
- }
- return c.OK(models.WhoAmIResponse{
- Host: c.Request().Host,
- RealIp: c.RealIP(),
- CurrentUser: models.UserInfo{
- Name: c.Header("current_user_name"),
- Email: email,
- Role: c.Header("current_user_role"),
- },
- })
-}
-func (bc *BookController) GetBooks(c okapi.Context) error {
- _, err := bc.readBooksFromFile()
- if err != nil {
- logger.Error("Error reading books from file", "error", err)
- return c.ErrorInternalServerError(models.ErrorResponse{Success: false, Status: http.StatusInternalServerError, Details: err.Error()})
- }
- return c.OK(books)
-}
-
-func (bc *BookController) CreateBook(c okapi.Context) error {
- // Simulate creating a book in a database
- book := &models.Book{}
- err := c.Bind(book)
- if err != nil {
- return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
- }
- book.Id = len(books) + 1
- books = append(books, book)
- response := models.Response{
- Success: true,
- Message: "Book created successfully",
- Data: *book,
- }
- return c.OK(response)
-}
-func (bc *BookController) GetBook(c okapi.Context) error {
- id := c.Param("id")
- i, err := strconv.Atoi(id)
- if err != nil {
- return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
- }
- // Simulate a fetching book from a database
-
- _, err = bc.readBooksFromFile()
- if err != nil {
- logger.Error("Error reading books from file", "error", err)
- return c.ErrorInternalServerError(models.ErrorResponse{Success: false, Status: http.StatusInternalServerError, Details: err.Error()})
- }
- for _, book := range books {
- if book.Id == i {
- return c.OK(book)
- }
- }
- return c.AbortNotFound("Book not found")
-}
-
-// ******************** AuthController *****************
-
-func (bc *AuthController) Login(c okapi.Context) error {
- authRequest := &models.AuthRequest{}
- err := c.Bind(authRequest)
- if err != nil {
- return c.ErrorBadRequest(models.ErrorResponse{Success: false, Status: http.StatusBadRequest, Details: err.Error()})
- }
- // Validate the authRequest and generate a JWT token
- authResponse, err := middlewares.Login(authRequest)
- if err != nil {
- return c.ErrorUnauthorized(authResponse)
- }
- return c.OK(authResponse)
-}
-func (bc *AuthController) WhoAmI(c okapi.Context) error {
- //Get User Information from the context, shared by the JWT middleware using forwardClaims
- email := c.GetString("email")
- if email == "" {
- return c.AbortUnauthorized("Unauthorized", fmt.Errorf("user not authenticated"))
- }
-
- c.Response().Header().Set("X-Okapi-User", email)
- c.Response().Header().Set("X-Okapi-User-Name", c.GetString("name"))
- c.Response().Header().Set("X-Okapi-Role", c.GetString("role"))
- // Respond with the current user information
- return c.OK(models.UserInfo{
- Email: email,
- Role: c.GetString("role"),
- Name: c.GetString("name"),
- },
- )
-}
-
-func (bc *BookController) readBooksFromFile() ([]*models.Book, error) {
- if books != nil && len(books) > 3 {
- return books, nil
- }
- booksFile, err := os.ReadFile("data/books.json")
- if err != nil {
- logger.Error("Error reading books file", "error", err)
- return nil, fmt.Errorf("failed to read books data: %w", err)
- }
- err = json.Unmarshal(booksFile, &books)
- if err != nil {
- logger.Error("Error unmarshalling books data", "error", err)
- return nil, fmt.Errorf("failed to parse books data: %w", err)
- }
- return books, nil
-}
diff --git a/data/book.go b/data/book.go
new file mode 100644
index 0000000..804624b
--- /dev/null
+++ b/data/book.go
@@ -0,0 +1,1424 @@
+package data
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/jkaninda/logger"
+ "github.com/jkaninda/okapi-example/models"
+)
+
+func Books() ([]*models.Book, error) {
+ books := []*models.Book{}
+ err := json.Unmarshal([]byte(booksData), &books)
+ if err != nil {
+ logger.Error("Error unmarshalling books data", "error", err)
+ return nil, fmt.Errorf("failed to parse books data: %w", err)
+ }
+ return books, nil
+}
+
+const booksData = `
+[
+ {
+ "id": 1,
+ "title": "The Kubernetes Bible",
+ "price": 100,
+ "year": 2022,
+ "author": "Nassim Kebbani, Piotr Tylenda",
+ "country": "USA",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+1",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
+ "pages": 652,
+ "createdAt": "2025-07-25T17:31:54.239027+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239027+02:00"
+ },
+ {
+ "id": 2,
+ "title": "Kubernetes - An Enterprise Guide",
+ "price": 110,
+ "year": 2022,
+ "author": "Marc Boorshtein, Scott Surovich",
+ "country": "USA",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+2",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
+ "pages": 652,
+ "createdAt": "2025-07-25T17:31:54.239027+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239027+02:00"
+ },
+ {
+ "id": 3,
+ "title": "System Design Interview Vol1",
+ "price": 120,
+ "year": 2022,
+ "author": "Alex U & Sahn Lam",
+ "country": "USA",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+3",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
+ "pages": 652,
+ "createdAt": "2025-07-25T17:31:54.239027+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239027+02:00"
+ },
+ {
+ "id": 4,
+ "title": "System Design Interview Vol2",
+ "price": 130,
+ "year": 2022,
+ "author": "Alex U & Sahn Lam",
+ "country": "USA",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+4",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
+ "pages": 652,
+ "createdAt": "2025-07-25T17:31:54.239027+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239027+02:00"
+ },
+ {
+ "id": 5,
+ "title": "Things Fall Apart",
+ "price": 140,
+ "year": 1958,
+ "author": "Chinua Achebe",
+ "country": "Nigeria",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+5",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
+ "pages": 209,
+ "createdAt": "2025-07-25T17:31:54.239028+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239028+02:00"
+ },
+ {
+ "id": 6,
+ "title": "Fairy tales",
+ "price": 150,
+ "year": 1836,
+ "author": "Hans Christian Andersen",
+ "country": "Denmark",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+6",
+ "language": "Danish",
+ "link": "https://en.wikipedia.org/wiki/Fairy_Tales_Told_for_Children._First_Collection.\n",
+ "pages": 784,
+ "createdAt": "2025-07-25T17:31:54.239028+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239028+02:00"
+ },
+ {
+ "id": 7,
+ "title": "The Divine Comedy",
+ "price": 160,
+ "year": 1315,
+ "author": "Dante Alighieri",
+ "country": "Italy",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+7",
+ "language": "Italian",
+ "link": "https://en.wikipedia.org/wiki/Divine_Comedy\n",
+ "pages": 928,
+ "createdAt": "2025-07-25T17:31:54.239028+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239028+02:00"
+ },
+ {
+ "id": 8,
+ "title": "The Epic Of Gilgamesh",
+ "price": 170,
+ "year": -1700,
+ "author": "Unknown",
+ "country": "Sumer and Akkadian Empire",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+8",
+ "language": "Akkadian",
+ "link": "https://en.wikipedia.org/wiki/Epic_of_Gilgamesh\n",
+ "pages": 160,
+ "createdAt": "2025-07-25T17:31:54.239028+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239028+02:00"
+ },
+ {
+ "id": 9,
+ "title": "The Book Of Job",
+ "price": 180,
+ "year": -600,
+ "author": "Unknown",
+ "country": "Achaemenid Empire",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+9",
+ "language": "Hebrew",
+ "link": "https://en.wikipedia.org/wiki/Book_of_Job\n",
+ "pages": 176,
+ "createdAt": "2025-07-25T17:31:54.239028+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239029+02:00"
+ },
+ {
+ "id": 10,
+ "title": "One Thousand and One Nights",
+ "price": 190,
+ "year": 1200,
+ "author": "Unknown",
+ "country": "India/Iran/Iraq/Egypt/Tajikistan",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+10",
+ "language": "Arabic",
+ "link": "https://en.wikipedia.org/wiki/One_Thousand_and_One_Nights\n",
+ "pages": 288,
+ "createdAt": "2025-07-25T17:31:54.239029+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239029+02:00"
+ },
+ {
+ "id": 11,
+ "title": "Nj\u00e1l's Saga",
+ "price": 200,
+ "year": 1350,
+ "author": "Unknown",
+ "country": "Iceland",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+11",
+ "language": "Old Norse",
+ "link": "https://en.wikipedia.org/wiki/Nj%C3%A1ls_saga\n",
+ "pages": 384,
+ "createdAt": "2025-07-25T17:31:54.239029+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239029+02:00"
+ },
+ {
+ "id": 12,
+ "title": "Pride and Prejudice",
+ "price": 210,
+ "year": 1813,
+ "author": "Jane Austen",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+12",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Pride_and_Prejudice\n",
+ "pages": 226,
+ "createdAt": "2025-07-25T17:31:54.239029+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239029+02:00"
+ },
+ {
+ "id": 13,
+ "title": "Le P\u00e8re Goriot",
+ "price": 220,
+ "year": 1835,
+ "author": "Honor\u00e9 de Balzac",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+13",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Le_P%C3%A8re_Goriot\n",
+ "pages": 443,
+ "createdAt": "2025-07-25T17:31:54.239029+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239029+02:00"
+ },
+ {
+ "id": 14,
+ "title": "Molloy, Malone Dies, The Unnamable, the trilogy",
+ "price": 230,
+ "year": 1952,
+ "author": "Samuel Beckett",
+ "country": "Republic of Ireland",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+14",
+ "language": "French, English",
+ "link": "https://en.wikipedia.org/wiki/Molloy_(novel)\n",
+ "pages": 256,
+ "createdAt": "2025-07-25T17:31:54.239029+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23903+02:00"
+ },
+ {
+ "id": 15,
+ "title": "The Decameron",
+ "price": 240,
+ "year": 1351,
+ "author": "Giovanni Boccaccio",
+ "country": "Italy",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+15",
+ "language": "Italian",
+ "link": "https://en.wikipedia.org/wiki/The_Decameron\n",
+ "pages": 1024,
+ "createdAt": "2025-07-25T17:31:54.23903+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23903+02:00"
+ },
+ {
+ "id": 16,
+ "title": "Ficciones",
+ "price": 250,
+ "year": 1965,
+ "author": "Jorge Luis Borges",
+ "country": "Argentina",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+16",
+ "language": "Spanish",
+ "link": "https://en.wikipedia.org/wiki/Ficciones\n",
+ "pages": 224,
+ "createdAt": "2025-07-25T17:31:54.23903+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23903+02:00"
+ },
+ {
+ "id": 17,
+ "title": "Wuthering Heights",
+ "price": 260,
+ "year": 1847,
+ "author": "Emily Bront\u00eb",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+17",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Wuthering_Heights\n",
+ "pages": 342,
+ "createdAt": "2025-07-25T17:31:54.23903+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23903+02:00"
+ },
+ {
+ "id": 18,
+ "title": "The Stranger",
+ "price": 270,
+ "year": 1942,
+ "author": "Albert Camus",
+ "country": "Algeria, French Empire",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+18",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/The_Stranger_(novel)\n",
+ "pages": 185,
+ "createdAt": "2025-07-25T17:31:54.23903+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23903+02:00"
+ },
+ {
+ "id": 19,
+ "title": "Poems",
+ "price": 280,
+ "year": 1952,
+ "author": "Paul Celan",
+ "country": "Romania, France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+19",
+ "language": "German",
+ "link": "\n",
+ "pages": 320,
+ "createdAt": "2025-07-25T17:31:54.23903+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23903+02:00"
+ },
+ {
+ "id": 20,
+ "title": "Journey to the End of the Night",
+ "price": 290,
+ "year": 1932,
+ "author": "Louis-Ferdinand C\u00e9line",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+20",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Journey_to_the_End_of_the_Night\n",
+ "pages": 505,
+ "createdAt": "2025-07-25T17:31:54.23903+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 21,
+ "title": "Don Quijote De La Mancha",
+ "price": 300,
+ "year": 1610,
+ "author": "Miguel de Cervantes",
+ "country": "Spain",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+21",
+ "language": "Spanish",
+ "link": "https://en.wikipedia.org/wiki/Don_Quixote\n",
+ "pages": 1056,
+ "createdAt": "2025-07-25T17:31:54.239031+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 22,
+ "title": "The Canterbury Tales",
+ "price": 310,
+ "year": 1450,
+ "author": "Geoffrey Chaucer",
+ "country": "England",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+22",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/The_Canterbury_Tales\n",
+ "pages": 544,
+ "createdAt": "2025-07-25T17:31:54.239031+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 23,
+ "title": "Stories",
+ "price": 320,
+ "year": 1886,
+ "author": "Anton Chekhov",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+23",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/List_of_short_stories_by_Anton_Chekhov\n",
+ "pages": 194,
+ "createdAt": "2025-07-25T17:31:54.239031+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 24,
+ "title": "Nostromo",
+ "price": 330,
+ "year": 1904,
+ "author": "Joseph Conrad",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+24",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Nostromo\n",
+ "pages": 320,
+ "createdAt": "2025-07-25T17:31:54.239031+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 25,
+ "title": "Great Expectations",
+ "price": 340,
+ "year": 1861,
+ "author": "Charles Dickens",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+25",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Great_Expectations\n",
+ "pages": 194,
+ "createdAt": "2025-07-25T17:31:54.239031+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 26,
+ "title": "Jacques the Fatalist",
+ "price": 350,
+ "year": 1796,
+ "author": "Denis Diderot",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+26",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Jacques_the_Fatalist\n",
+ "pages": 596,
+ "createdAt": "2025-07-25T17:31:54.239031+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239031+02:00"
+ },
+ {
+ "id": 27,
+ "title": "Berlin Alexanderplatz",
+ "price": 360,
+ "year": 1929,
+ "author": "Alfred D\u00f6blin",
+ "country": "Germany",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+27",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/Berlin_Alexanderplatz\n",
+ "pages": 600,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239032+02:00"
+ },
+ {
+ "id": 28,
+ "title": "Crime and Punishment",
+ "price": 370,
+ "year": 1866,
+ "author": "Fyodor Dostoevsky",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+28",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/Crime_and_Punishment\n",
+ "pages": 551,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239032+02:00"
+ },
+ {
+ "id": 29,
+ "title": "The Idiot",
+ "price": 380,
+ "year": 1869,
+ "author": "Fyodor Dostoevsky",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+29",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/The_Idiot\n",
+ "pages": 656,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239032+02:00"
+ },
+ {
+ "id": 30,
+ "title": "The Possessed",
+ "price": 390,
+ "year": 1872,
+ "author": "Fyodor Dostoevsky",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+30",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/Demons_(Dostoyevsky_novel)\n",
+ "pages": 768,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239032+02:00"
+ },
+ {
+ "id": 31,
+ "title": "The Brothers Karamazov",
+ "price": 400,
+ "year": 1880,
+ "author": "Fyodor Dostoevsky",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+31",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/The_Brothers_Karamazov\n",
+ "pages": 824,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239032+02:00"
+ },
+ {
+ "id": 32,
+ "title": "Middlemarch",
+ "price": 410,
+ "year": 1871,
+ "author": "George Eliot",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+32",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Middlemarch\n",
+ "pages": 800,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239032+02:00"
+ },
+ {
+ "id": 33,
+ "title": "Invisible Man",
+ "price": 420,
+ "year": 1952,
+ "author": "Ralph Ellison",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+33",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Invisible_Man\n",
+ "pages": 581,
+ "createdAt": "2025-07-25T17:31:54.239032+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239033+02:00"
+ },
+ {
+ "id": 34,
+ "title": "Medea",
+ "price": 430,
+ "year": -431,
+ "author": "Euripides",
+ "country": "Greece",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+34",
+ "language": "Greek",
+ "link": "https://en.wikipedia.org/wiki/Medea_(play)\n",
+ "pages": 104,
+ "createdAt": "2025-07-25T17:31:54.239033+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239033+02:00"
+ },
+ {
+ "id": 35,
+ "title": "Absalom, Absalom!",
+ "price": 440,
+ "year": 1936,
+ "author": "William Faulkner",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+35",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Absalom,_Absalom!\n",
+ "pages": 313,
+ "createdAt": "2025-07-25T17:31:54.239033+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239033+02:00"
+ },
+ {
+ "id": 36,
+ "title": "The Sound and the Fury",
+ "price": 450,
+ "year": 1929,
+ "author": "William Faulkner",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+36",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/The_Sound_and_the_Fury\n",
+ "pages": 326,
+ "createdAt": "2025-07-25T17:31:54.239033+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239033+02:00"
+ },
+ {
+ "id": 37,
+ "title": "Madame Bovary",
+ "price": 460,
+ "year": 1857,
+ "author": "Gustave Flaubert",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+37",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Madame_Bovary\n",
+ "pages": 528,
+ "createdAt": "2025-07-25T17:31:54.239033+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239033+02:00"
+ },
+ {
+ "id": 38,
+ "title": "Sentimental Education",
+ "price": 470,
+ "year": 1869,
+ "author": "Gustave Flaubert",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+38",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Sentimental_Education\n",
+ "pages": 606,
+ "createdAt": "2025-07-25T17:31:54.239033+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239033+02:00"
+ },
+ {
+ "id": 39,
+ "title": "Gypsy Ballads",
+ "price": 480,
+ "year": 1928,
+ "author": "Federico Garc\u00eda Lorca",
+ "country": "Spain",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+39",
+ "language": "Spanish",
+ "link": "https://en.wikipedia.org/wiki/Gypsy_Ballads\n",
+ "pages": 218,
+ "createdAt": "2025-07-25T17:31:54.239033+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 40,
+ "title": "One Hundred Years of Solitude",
+ "price": 490,
+ "year": 1967,
+ "author": "Gabriel Garc\u00eda M\u00e1rquez",
+ "country": "Colombia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+40",
+ "language": "Spanish",
+ "link": "https://en.wikipedia.org/wiki/One_Hundred_Years_of_Solitude\n",
+ "pages": 417,
+ "createdAt": "2025-07-25T17:31:54.239034+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 41,
+ "title": "Love in the Time of Cholera",
+ "price": 500,
+ "year": 1985,
+ "author": "Gabriel Garc\u00eda M\u00e1rquez",
+ "country": "Colombia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+41",
+ "language": "Spanish",
+ "link": "https://en.wikipedia.org/wiki/Love_in_the_Time_of_Cholera\n",
+ "pages": 368,
+ "createdAt": "2025-07-25T17:31:54.239034+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 42,
+ "title": "Faust",
+ "price": 510,
+ "year": 1832,
+ "author": "Johann Wolfgang von Goethe",
+ "country": "Saxe-Weimar",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+42",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/Goethe%27s_Faust\n",
+ "pages": 158,
+ "createdAt": "2025-07-25T17:31:54.239034+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 43,
+ "title": "Dead Souls",
+ "price": 520,
+ "year": 1842,
+ "author": "Nikolai Gogol",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+43",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/Dead_Souls\n",
+ "pages": 432,
+ "createdAt": "2025-07-25T17:31:54.239034+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 44,
+ "title": "The Tin Drum",
+ "price": 530,
+ "year": 1959,
+ "author": "G\u00fcnter Grass",
+ "country": "Germany",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+44",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/The_Tin_Drum\n",
+ "pages": 600,
+ "createdAt": "2025-07-25T17:31:54.239034+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 45,
+ "title": "The Devil to Pay in the Backlands",
+ "price": 540,
+ "year": 1956,
+ "author": "Jo\u00e3o Guimar\u00e3es Rosa",
+ "country": "Brazil",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+45",
+ "language": "Portuguese",
+ "link": "https://en.wikipedia.org/wiki/The_Devil_to_Pay_in_the_Backlands\n",
+ "pages": 494,
+ "createdAt": "2025-07-25T17:31:54.239034+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239034+02:00"
+ },
+ {
+ "id": 46,
+ "title": "Hunger",
+ "price": 550,
+ "year": 1890,
+ "author": "Knut Hamsun",
+ "country": "Norway",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+46",
+ "language": "Norwegian",
+ "link": "https://en.wikipedia.org/wiki/Hunger_(Hamsun_novel)\n",
+ "pages": 176,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239035+02:00"
+ },
+ {
+ "id": 47,
+ "title": "The Old Man and the Sea",
+ "price": 560,
+ "year": 1952,
+ "author": "Ernest Hemingway",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+47",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/The_Old_Man_and_the_Sea\n",
+ "pages": 128,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239035+02:00"
+ },
+ {
+ "id": 48,
+ "title": "Iliad",
+ "price": 570,
+ "year": -735,
+ "author": "Homer",
+ "country": "Greece",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+48",
+ "language": "Greek",
+ "link": "https://en.wikipedia.org/wiki/Iliad\n",
+ "pages": 608,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239035+02:00"
+ },
+ {
+ "id": 49,
+ "title": "Odyssey",
+ "price": 580,
+ "year": -800,
+ "author": "Homer",
+ "country": "Greece",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+49",
+ "language": "Greek",
+ "link": "https://en.wikipedia.org/wiki/Odyssey\n",
+ "pages": 374,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239035+02:00"
+ },
+ {
+ "id": 50,
+ "title": "A Doll's House",
+ "price": 590,
+ "year": 1879,
+ "author": "Henrik Ibsen",
+ "country": "Norway",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+50",
+ "language": "Norwegian",
+ "link": "https://en.wikipedia.org/wiki/A_Doll%27s_House\n",
+ "pages": 68,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239035+02:00"
+ },
+ {
+ "id": 51,
+ "title": "Ulysses",
+ "price": 600,
+ "year": 1922,
+ "author": "James Joyce",
+ "country": "Irish Free State",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+51",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Ulysses_(novel)\n",
+ "pages": 228,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239035+02:00"
+ },
+ {
+ "id": 52,
+ "title": "Stories",
+ "price": 610,
+ "year": 1924,
+ "author": "Franz Kafka",
+ "country": "Czechoslovakia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+52",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/Franz_Kafka_bibliography#Short_stories\n",
+ "pages": 488,
+ "createdAt": "2025-07-25T17:31:54.239035+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239036+02:00"
+ },
+ {
+ "id": 53,
+ "title": "The Trial",
+ "price": 620,
+ "year": 1925,
+ "author": "Franz Kafka",
+ "country": "Czechoslovakia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+53",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/The_Trial\n",
+ "pages": 160,
+ "createdAt": "2025-07-25T17:31:54.239036+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239036+02:00"
+ },
+ {
+ "id": 54,
+ "title": "The Castle",
+ "price": 630,
+ "year": 1926,
+ "author": "Franz Kafka",
+ "country": "Czechoslovakia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+54",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/The_Castle_(novel)\n",
+ "pages": 352,
+ "createdAt": "2025-07-25T17:31:54.239036+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239036+02:00"
+ },
+ {
+ "id": 55,
+ "title": "The recognition of Shakuntala",
+ "price": 640,
+ "year": 150,
+ "author": "K\u0101lid\u0101sa",
+ "country": "India",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+55",
+ "language": "Sanskrit",
+ "link": "https://en.wikipedia.org/wiki/Abhij%C3%B1%C4%81na%C5%9B%C4%81kuntalam\n",
+ "pages": 147,
+ "createdAt": "2025-07-25T17:31:54.239036+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239036+02:00"
+ },
+ {
+ "id": 56,
+ "title": "The Sound of the Mountain",
+ "price": 650,
+ "year": 1954,
+ "author": "Yasunari Kawabata",
+ "country": "Japan",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+56",
+ "language": "Japanese",
+ "link": "https://en.wikipedia.org/wiki/The_Sound_of_the_Mountain\n",
+ "pages": 288,
+ "createdAt": "2025-07-25T17:31:54.239036+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239036+02:00"
+ },
+ {
+ "id": 57,
+ "title": "Zorba the Greek",
+ "price": 660,
+ "year": 1946,
+ "author": "Nikos Kazantzakis",
+ "country": "Greece",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+57",
+ "language": "Greek",
+ "link": "https://en.wikipedia.org/wiki/Zorba_the_Greek\n",
+ "pages": 368,
+ "createdAt": "2025-07-25T17:31:54.239036+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239036+02:00"
+ },
+ {
+ "id": 58,
+ "title": "Sons and Lovers",
+ "price": 670,
+ "year": 1913,
+ "author": "D. H. Lawrence",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+58",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Sons_and_Lovers\n",
+ "pages": 432,
+ "createdAt": "2025-07-25T17:31:54.239036+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 59,
+ "title": "Independent People",
+ "price": 680,
+ "year": 1934,
+ "author": "Halld\u00f3r Laxness",
+ "country": "Iceland",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+59",
+ "language": "Icelandic",
+ "link": "https://en.wikipedia.org/wiki/Independent_People\n",
+ "pages": 470,
+ "createdAt": "2025-07-25T17:31:54.239037+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 60,
+ "title": "Poems",
+ "price": 690,
+ "year": 1818,
+ "author": "Giacomo Leopardi",
+ "country": "Italy",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+60",
+ "language": "Italian",
+ "link": "\n",
+ "pages": 184,
+ "createdAt": "2025-07-25T17:31:54.239037+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 61,
+ "title": "The Golden Notebook",
+ "price": 700,
+ "year": 1962,
+ "author": "Doris Lessing",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+61",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/The_Golden_Notebook\n",
+ "pages": 688,
+ "createdAt": "2025-07-25T17:31:54.239037+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 62,
+ "title": "Pippi Longstocking",
+ "price": 710,
+ "year": 1945,
+ "author": "Astrid Lindgren",
+ "country": "Sweden",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+62",
+ "language": "Swedish",
+ "link": "https://en.wikipedia.org/wiki/Pippi_Longstocking\n",
+ "pages": 160,
+ "createdAt": "2025-07-25T17:31:54.239037+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 63,
+ "title": "Diary of a Madman",
+ "price": 720,
+ "year": 1918,
+ "author": "Lu Xun",
+ "country": "China",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+63",
+ "language": "Chinese",
+ "link": "https://en.wikipedia.org/wiki/A_Madman%27s_Diary\n",
+ "pages": 389,
+ "createdAt": "2025-07-25T17:31:54.239037+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 64,
+ "title": "Children of Gebelawi",
+ "price": 730,
+ "year": 1959,
+ "author": "Naguib Mahfouz",
+ "country": "Egypt",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+64",
+ "language": "Arabic",
+ "link": "https://en.wikipedia.org/wiki/Children_of_Gebelawi\n",
+ "pages": 355,
+ "createdAt": "2025-07-25T17:31:54.239037+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239037+02:00"
+ },
+ {
+ "id": 65,
+ "title": "Buddenbrooks",
+ "price": 740,
+ "year": 1901,
+ "author": "Thomas Mann",
+ "country": "Germany",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+65",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/Buddenbrooks\n",
+ "pages": 736,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239038+02:00"
+ },
+ {
+ "id": 66,
+ "title": "The Magic Mountain",
+ "price": 750,
+ "year": 1924,
+ "author": "Thomas Mann",
+ "country": "Germany",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+66",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/The_Magic_Mountain\n",
+ "pages": 720,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239038+02:00"
+ },
+ {
+ "id": 67,
+ "title": "Moby Dick",
+ "price": 760,
+ "year": 1851,
+ "author": "Herman Melville",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+67",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Moby-Dick\n",
+ "pages": 378,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239038+02:00"
+ },
+ {
+ "id": 68,
+ "title": "Essays",
+ "price": 770,
+ "year": 1595,
+ "author": "Michel de Montaigne",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+68",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Essays_(Montaigne)\n",
+ "pages": 404,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239038+02:00"
+ },
+ {
+ "id": 69,
+ "title": "History",
+ "price": 780,
+ "year": 1974,
+ "author": "Elsa Morante",
+ "country": "Italy",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+69",
+ "language": "Italian",
+ "link": "https://en.wikipedia.org/wiki/History_(novel)\n",
+ "pages": 600,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239038+02:00"
+ },
+ {
+ "id": 70,
+ "title": "Beloved",
+ "price": 790,
+ "year": 1987,
+ "author": "Toni Morrison",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+70",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Beloved_(novel)\n",
+ "pages": 321,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239038+02:00"
+ },
+ {
+ "id": 71,
+ "title": "The Tale of Genji",
+ "price": 800,
+ "year": 1006,
+ "author": "Murasaki Shikibu",
+ "country": "Japan",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+71",
+ "language": "Japanese",
+ "link": "https://en.wikipedia.org/wiki/The_Tale_of_Genji\n",
+ "pages": 1360,
+ "createdAt": "2025-07-25T17:31:54.239038+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239039+02:00"
+ },
+ {
+ "id": 72,
+ "title": "The Man Without Qualities",
+ "price": 810,
+ "year": 1931,
+ "author": "Robert Musil",
+ "country": "Austria",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+72",
+ "language": "German",
+ "link": "https://en.wikipedia.org/wiki/The_Man_Without_Qualities\n",
+ "pages": 365,
+ "createdAt": "2025-07-25T17:31:54.239039+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239039+02:00"
+ },
+ {
+ "id": 73,
+ "title": "Lolita",
+ "price": 820,
+ "year": 1955,
+ "author": "Vladimir Nabokov",
+ "country": "Russia/United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+73",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Lolita\n",
+ "pages": 317,
+ "createdAt": "2025-07-25T17:31:54.239039+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239039+02:00"
+ },
+ {
+ "id": 74,
+ "title": "Nineteen Eighty-Four",
+ "price": 830,
+ "year": 1949,
+ "author": "George Orwell",
+ "country": "United Kingdom",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+74",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Nineteen_Eighty-Four\n",
+ "pages": 272,
+ "createdAt": "2025-07-25T17:31:54.239039+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239039+02:00"
+ },
+ {
+ "id": 75,
+ "title": "Metamorphoses",
+ "price": 840,
+ "year": 100,
+ "author": "Ovid",
+ "country": "Roman Empire",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+75",
+ "language": "Classical Latin",
+ "link": "https://en.wikipedia.org/wiki/Metamorphoses\n",
+ "pages": 576,
+ "createdAt": "2025-07-25T17:31:54.239039+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239039+02:00"
+ },
+ {
+ "id": 76,
+ "title": "The Book of Disquiet",
+ "price": 850,
+ "year": 1928,
+ "author": "Fernando Pessoa",
+ "country": "Portugal",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+76",
+ "language": "Portuguese",
+ "link": "https://en.wikipedia.org/wiki/The_Book_of_Disquiet\n",
+ "pages": 272,
+ "createdAt": "2025-07-25T17:31:54.239039+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239039+02:00"
+ },
+ {
+ "id": 77,
+ "title": "Tales",
+ "price": 860,
+ "year": 1950,
+ "author": "Edgar Allan Poe",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+77",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Edgar_Allan_Poe_bibliography#Tales\n",
+ "pages": 842,
+ "createdAt": "2025-07-25T17:31:54.239039+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 78,
+ "title": "In Search of Lost Time",
+ "price": 870,
+ "year": 1920,
+ "author": "Marcel Proust",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+78",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/In_Search_of_Lost_Time\n",
+ "pages": 2408,
+ "createdAt": "2025-07-25T17:31:54.23904+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 79,
+ "title": "Gargantua and Pantagruel",
+ "price": 880,
+ "year": 1533,
+ "author": "Fran\u00e7ois Rabelais",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+79",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/Gargantua_and_Pantagruel\n",
+ "pages": 623,
+ "createdAt": "2025-07-25T17:31:54.23904+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 80,
+ "title": "Pedro P\u00e1ramo",
+ "price": 890,
+ "year": 1955,
+ "author": "Juan Rulfo",
+ "country": "Mexico",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+80",
+ "language": "Spanish",
+ "link": "https://en.wikipedia.org/wiki/Pedro_P%C3%A1ramo\n",
+ "pages": 124,
+ "createdAt": "2025-07-25T17:31:54.23904+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 81,
+ "title": "The Masnavi",
+ "price": 900,
+ "year": 1236,
+ "author": "Rumi",
+ "country": "Sultanate of Rum",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+81",
+ "language": "Persian",
+ "link": "https://en.wikipedia.org/wiki/Masnavi\n",
+ "pages": 438,
+ "createdAt": "2025-07-25T17:31:54.23904+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 82,
+ "title": "Midnight's Children",
+ "price": 910,
+ "year": 1981,
+ "author": "Salman Rushdie",
+ "country": "United Kingdom, India",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+82",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Midnight%27s_Children\n",
+ "pages": 536,
+ "createdAt": "2025-07-25T17:31:54.23904+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 83,
+ "title": "Bostan",
+ "price": 920,
+ "year": 1257,
+ "author": "Saadi",
+ "country": "Persia, Persian Empire",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+83",
+ "language": "Persian",
+ "link": "https://en.wikipedia.org/wiki/Bustan_(book)\n",
+ "pages": 298,
+ "createdAt": "2025-07-25T17:31:54.23904+02:00",
+ "updatedAt": "2025-07-25T17:31:54.23904+02:00"
+ },
+ {
+ "id": 84,
+ "title": "Season of Migration to the North",
+ "price": 930,
+ "year": 1966,
+ "author": "Tayeb Salih",
+ "country": "Sudan",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+84",
+ "language": "Arabic",
+ "link": "https://en.wikipedia.org/wiki/Season_of_Migration_to_the_North\n",
+ "pages": 139,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239041+02:00"
+ },
+ {
+ "id": 85,
+ "title": "Blindness",
+ "price": 940,
+ "year": 1995,
+ "author": "Jos\u00e9 Saramago",
+ "country": "Portugal",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+85",
+ "language": "Portuguese",
+ "link": "https://en.wikipedia.org/wiki/Blindness_(novel)\n",
+ "pages": 352,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239041+02:00"
+ },
+ {
+ "id": 86,
+ "title": "Hamlet",
+ "price": 950,
+ "year": 1603,
+ "author": "William Shakespeare",
+ "country": "England",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+86",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Hamlet\n",
+ "pages": 432,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239041+02:00"
+ },
+ {
+ "id": 87,
+ "title": "King Lear",
+ "price": 960,
+ "year": 1608,
+ "author": "William Shakespeare",
+ "country": "England",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+87",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/King_Lear\n",
+ "pages": 384,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239041+02:00"
+ },
+ {
+ "id": 88,
+ "title": "Othello",
+ "price": 970,
+ "year": 1609,
+ "author": "William Shakespeare",
+ "country": "England",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+88",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Othello\n",
+ "pages": 314,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239041+02:00"
+ },
+ {
+ "id": 89,
+ "title": "Oedipus the King",
+ "price": 980,
+ "year": -430,
+ "author": "Sophocles",
+ "country": "Greece",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+89",
+ "language": "Greek",
+ "link": "https://en.wikipedia.org/wiki/Oedipus_the_King\n",
+ "pages": 88,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239041+02:00"
+ },
+ {
+ "id": 90,
+ "title": "The Red and the Black",
+ "price": 990,
+ "year": 1830,
+ "author": "Stendhal",
+ "country": "France",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+90",
+ "language": "French",
+ "link": "https://en.wikipedia.org/wiki/The_Red_and_the_Black\n",
+ "pages": 576,
+ "createdAt": "2025-07-25T17:31:54.239041+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239042+02:00"
+ },
+ {
+ "id": 91,
+ "title": "The Life And Opinions of Tristram Shandy",
+ "price": 1000,
+ "year": 1760,
+ "author": "Laurence Sterne",
+ "country": "England",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+91",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/The_Life_and_Opinions_of_Tristram_Shandy,_Gentleman\n",
+ "pages": 640,
+ "createdAt": "2025-07-25T17:31:54.239042+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239042+02:00"
+ },
+ {
+ "id": 92,
+ "title": "Confessions of Zeno",
+ "price": 1010,
+ "year": 1923,
+ "author": "Italo Svevo",
+ "country": "Italy",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+92",
+ "language": "Italian",
+ "link": "https://en.wikipedia.org/wiki/Zeno%27s_Conscience\n",
+ "pages": 412,
+ "createdAt": "2025-07-25T17:31:54.239042+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239042+02:00"
+ },
+ {
+ "id": 93,
+ "title": "Gulliver's Travels",
+ "price": 1020,
+ "year": 1726,
+ "author": "Jonathan Swift",
+ "country": "Ireland",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+93",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Gulliver%27s_Travels\n",
+ "pages": 178,
+ "createdAt": "2025-07-25T17:31:54.239042+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239042+02:00"
+ },
+ {
+ "id": 94,
+ "title": "War and Peace",
+ "price": 1030,
+ "year": 1867,
+ "author": "Leo Tolstoy",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+94",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/War_and_Peace\n",
+ "pages": 1296,
+ "createdAt": "2025-07-25T17:31:54.239042+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239042+02:00"
+ },
+ {
+ "id": 95,
+ "title": "Anna Karenina",
+ "price": 1040,
+ "year": 1877,
+ "author": "Leo Tolstoy",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+95",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/Anna_Karenina\n",
+ "pages": 864,
+ "createdAt": "2025-07-25T17:31:54.239042+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239042+02:00"
+ },
+ {
+ "id": 96,
+ "title": "The Death of Ivan Ilyich",
+ "price": 1050,
+ "year": 1886,
+ "author": "Leo Tolstoy",
+ "country": "Russia",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+96",
+ "language": "Russian",
+ "link": "https://en.wikipedia.org/wiki/The_Death_of_Ivan_Ilyich\n",
+ "pages": 92,
+ "createdAt": "2025-07-25T17:31:54.239042+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239043+02:00"
+ },
+ {
+ "id": 97,
+ "title": "The Adventures of Huckleberry Finn",
+ "price": 1060,
+ "year": 1884,
+ "author": "Mark Twain",
+ "country": "United States",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+97",
+ "language": "English",
+ "link": "https://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn\n",
+ "pages": 224,
+ "createdAt": "2025-07-25T17:31:54.239043+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239043+02:00"
+ },
+ {
+ "id": 98,
+ "title": "Ramayana",
+ "price": 1070,
+ "year": -450,
+ "author": "Valmiki",
+ "country": "India",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+98",
+ "language": "Sanskrit",
+ "link": "https://en.wikipedia.org/wiki/Ramayana\n",
+ "pages": 152,
+ "createdAt": "2025-07-25T17:31:54.239043+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239043+02:00"
+ },
+ {
+ "id": 99,
+ "title": "The Aeneid",
+ "price": 1080,
+ "year": -23,
+ "author": "Virgil",
+ "country": "Roman Empire",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+99",
+ "language": "Classical Latin",
+ "link": "https://en.wikipedia.org/wiki/Aeneid\n",
+ "pages": 442,
+ "createdAt": "2025-07-25T17:31:54.239043+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239043+02:00"
+ },
+ {
+ "id": 100,
+ "title": "Mahabharata",
+ "price": 1090,
+ "year": -700,
+ "author": "Vyasa",
+ "country": "India",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+100",
+ "language": "Sanskrit",
+ "link": "https://en.wikipedia.org/wiki/Mahabharata\n",
+ "pages": 276,
+ "createdAt": "2025-07-25T17:31:54.239043+02:00",
+ "updatedAt": "2025-07-25T17:31:54.239043+02:00"
+ }
+]
+`
diff --git a/data/books.json b/data/books.json
index 8b2b656..87109c4 100644
--- a/data/books.json
+++ b/data/books.json
@@ -6,7 +6,7 @@
"year": 2022,
"author": "Nassim Kebbani, Piotr Tylenda",
"country": "USA",
- "imageLink": "images/things-fall-apart.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+1",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
"pages": 652,
@@ -20,7 +20,7 @@
"year": 2022,
"author": "Marc Boorshtein, Scott Surovich",
"country": "USA",
- "imageLink": "images/things-fall-apart.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+2",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
"pages": 652,
@@ -34,7 +34,7 @@
"year": 2022,
"author": "Alex U & Sahn Lam",
"country": "USA",
- "imageLink": "images/things-fall-apart.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+3",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
"pages": 652,
@@ -48,7 +48,7 @@
"year": 2022,
"author": "Alex U & Sahn Lam",
"country": "USA",
- "imageLink": "images/things-fall-apart.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+4",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
"pages": 652,
@@ -62,7 +62,7 @@
"year": 1958,
"author": "Chinua Achebe",
"country": "Nigeria",
- "imageLink": "images/things-fall-apart.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+5",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Things_Fall_Apart\n",
"pages": 209,
@@ -76,7 +76,7 @@
"year": 1836,
"author": "Hans Christian Andersen",
"country": "Denmark",
- "imageLink": "images/fairy-tales.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+6",
"language": "Danish",
"link": "https://en.wikipedia.org/wiki/Fairy_Tales_Told_for_Children._First_Collection.\n",
"pages": 784,
@@ -90,7 +90,7 @@
"year": 1315,
"author": "Dante Alighieri",
"country": "Italy",
- "imageLink": "images/the-divine-comedy.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+7",
"language": "Italian",
"link": "https://en.wikipedia.org/wiki/Divine_Comedy\n",
"pages": 928,
@@ -104,7 +104,7 @@
"year": -1700,
"author": "Unknown",
"country": "Sumer and Akkadian Empire",
- "imageLink": "images/the-epic-of-gilgamesh.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+8",
"language": "Akkadian",
"link": "https://en.wikipedia.org/wiki/Epic_of_Gilgamesh\n",
"pages": 160,
@@ -118,7 +118,7 @@
"year": -600,
"author": "Unknown",
"country": "Achaemenid Empire",
- "imageLink": "images/the-book-of-job.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+9",
"language": "Hebrew",
"link": "https://en.wikipedia.org/wiki/Book_of_Job\n",
"pages": 176,
@@ -132,7 +132,7 @@
"year": 1200,
"author": "Unknown",
"country": "India/Iran/Iraq/Egypt/Tajikistan",
- "imageLink": "images/one-thousand-and-one-nights.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+10",
"language": "Arabic",
"link": "https://en.wikipedia.org/wiki/One_Thousand_and_One_Nights\n",
"pages": 288,
@@ -141,12 +141,12 @@
},
{
"id": 11,
- "title": "Njál's Saga",
+ "title": "Nj\u00e1l's Saga",
"price": 200,
"year": 1350,
"author": "Unknown",
"country": "Iceland",
- "imageLink": "images/njals-saga.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+11",
"language": "Old Norse",
"link": "https://en.wikipedia.org/wiki/Nj%C3%A1ls_saga\n",
"pages": 384,
@@ -160,7 +160,7 @@
"year": 1813,
"author": "Jane Austen",
"country": "United Kingdom",
- "imageLink": "images/pride-and-prejudice.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+12",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Pride_and_Prejudice\n",
"pages": 226,
@@ -169,12 +169,12 @@
},
{
"id": 13,
- "title": "Le Père Goriot",
+ "title": "Le P\u00e8re Goriot",
"price": 220,
"year": 1835,
- "author": "Honoré de Balzac",
+ "author": "Honor\u00e9 de Balzac",
"country": "France",
- "imageLink": "images/le-pere-goriot.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+13",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Le_P%C3%A8re_Goriot\n",
"pages": 443,
@@ -188,7 +188,7 @@
"year": 1952,
"author": "Samuel Beckett",
"country": "Republic of Ireland",
- "imageLink": "images/molloy-malone-dies-the-unnamable.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+14",
"language": "French, English",
"link": "https://en.wikipedia.org/wiki/Molloy_(novel)\n",
"pages": 256,
@@ -202,7 +202,7 @@
"year": 1351,
"author": "Giovanni Boccaccio",
"country": "Italy",
- "imageLink": "images/the-decameron.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+15",
"language": "Italian",
"link": "https://en.wikipedia.org/wiki/The_Decameron\n",
"pages": 1024,
@@ -216,7 +216,7 @@
"year": 1965,
"author": "Jorge Luis Borges",
"country": "Argentina",
- "imageLink": "images/ficciones.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+16",
"language": "Spanish",
"link": "https://en.wikipedia.org/wiki/Ficciones\n",
"pages": 224,
@@ -228,9 +228,9 @@
"title": "Wuthering Heights",
"price": 260,
"year": 1847,
- "author": "Emily Brontë",
+ "author": "Emily Bront\u00eb",
"country": "United Kingdom",
- "imageLink": "images/wuthering-heights.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+17",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Wuthering_Heights\n",
"pages": 342,
@@ -244,7 +244,7 @@
"year": 1942,
"author": "Albert Camus",
"country": "Algeria, French Empire",
- "imageLink": "images/l-etranger.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+18",
"language": "French",
"link": "https://en.wikipedia.org/wiki/The_Stranger_(novel)\n",
"pages": 185,
@@ -258,7 +258,7 @@
"year": 1952,
"author": "Paul Celan",
"country": "Romania, France",
- "imageLink": "images/poems-paul-celan.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+19",
"language": "German",
"link": "\n",
"pages": 320,
@@ -270,9 +270,9 @@
"title": "Journey to the End of the Night",
"price": 290,
"year": 1932,
- "author": "Louis-Ferdinand Céline",
+ "author": "Louis-Ferdinand C\u00e9line",
"country": "France",
- "imageLink": "images/voyage-au-bout-de-la-nuit.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+20",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Journey_to_the_End_of_the_Night\n",
"pages": 505,
@@ -286,7 +286,7 @@
"year": 1610,
"author": "Miguel de Cervantes",
"country": "Spain",
- "imageLink": "images/don-quijote-de-la-mancha.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+21",
"language": "Spanish",
"link": "https://en.wikipedia.org/wiki/Don_Quixote\n",
"pages": 1056,
@@ -300,7 +300,7 @@
"year": 1450,
"author": "Geoffrey Chaucer",
"country": "England",
- "imageLink": "images/the-canterbury-tales.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+22",
"language": "English",
"link": "https://en.wikipedia.org/wiki/The_Canterbury_Tales\n",
"pages": 544,
@@ -314,7 +314,7 @@
"year": 1886,
"author": "Anton Chekhov",
"country": "Russia",
- "imageLink": "images/stories-of-anton-chekhov.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+23",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/List_of_short_stories_by_Anton_Chekhov\n",
"pages": 194,
@@ -328,7 +328,7 @@
"year": 1904,
"author": "Joseph Conrad",
"country": "United Kingdom",
- "imageLink": "images/nostromo.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+24",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Nostromo\n",
"pages": 320,
@@ -342,7 +342,7 @@
"year": 1861,
"author": "Charles Dickens",
"country": "United Kingdom",
- "imageLink": "images/great-expectations.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+25",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Great_Expectations\n",
"pages": 194,
@@ -356,7 +356,7 @@
"year": 1796,
"author": "Denis Diderot",
"country": "France",
- "imageLink": "images/jacques-the-fatalist.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+26",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Jacques_the_Fatalist\n",
"pages": 596,
@@ -368,9 +368,9 @@
"title": "Berlin Alexanderplatz",
"price": 360,
"year": 1929,
- "author": "Alfred Döblin",
+ "author": "Alfred D\u00f6blin",
"country": "Germany",
- "imageLink": "images/berlin-alexanderplatz.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+27",
"language": "German",
"link": "https://en.wikipedia.org/wiki/Berlin_Alexanderplatz\n",
"pages": 600,
@@ -384,7 +384,7 @@
"year": 1866,
"author": "Fyodor Dostoevsky",
"country": "Russia",
- "imageLink": "images/crime-and-punishment.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+28",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/Crime_and_Punishment\n",
"pages": 551,
@@ -398,7 +398,7 @@
"year": 1869,
"author": "Fyodor Dostoevsky",
"country": "Russia",
- "imageLink": "images/the-idiot.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+29",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/The_Idiot\n",
"pages": 656,
@@ -412,7 +412,7 @@
"year": 1872,
"author": "Fyodor Dostoevsky",
"country": "Russia",
- "imageLink": "images/the-possessed.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+30",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/Demons_(Dostoyevsky_novel)\n",
"pages": 768,
@@ -426,7 +426,7 @@
"year": 1880,
"author": "Fyodor Dostoevsky",
"country": "Russia",
- "imageLink": "images/the-brothers-karamazov.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+31",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/The_Brothers_Karamazov\n",
"pages": 824,
@@ -440,7 +440,7 @@
"year": 1871,
"author": "George Eliot",
"country": "United Kingdom",
- "imageLink": "images/middlemarch.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+32",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Middlemarch\n",
"pages": 800,
@@ -454,7 +454,7 @@
"year": 1952,
"author": "Ralph Ellison",
"country": "United States",
- "imageLink": "images/invisible-man.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+33",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Invisible_Man\n",
"pages": 581,
@@ -468,7 +468,7 @@
"year": -431,
"author": "Euripides",
"country": "Greece",
- "imageLink": "images/medea.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+34",
"language": "Greek",
"link": "https://en.wikipedia.org/wiki/Medea_(play)\n",
"pages": 104,
@@ -482,7 +482,7 @@
"year": 1936,
"author": "William Faulkner",
"country": "United States",
- "imageLink": "images/absalom-absalom.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+35",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Absalom,_Absalom!\n",
"pages": 313,
@@ -496,7 +496,7 @@
"year": 1929,
"author": "William Faulkner",
"country": "United States",
- "imageLink": "images/the-sound-and-the-fury.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+36",
"language": "English",
"link": "https://en.wikipedia.org/wiki/The_Sound_and_the_Fury\n",
"pages": 326,
@@ -510,7 +510,7 @@
"year": 1857,
"author": "Gustave Flaubert",
"country": "France",
- "imageLink": "images/madame-bovary.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+37",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Madame_Bovary\n",
"pages": 528,
@@ -524,7 +524,7 @@
"year": 1869,
"author": "Gustave Flaubert",
"country": "France",
- "imageLink": "images/l-education-sentimentale.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+38",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Sentimental_Education\n",
"pages": 606,
@@ -536,9 +536,9 @@
"title": "Gypsy Ballads",
"price": 480,
"year": 1928,
- "author": "Federico García Lorca",
+ "author": "Federico Garc\u00eda Lorca",
"country": "Spain",
- "imageLink": "images/gypsy-ballads.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+39",
"language": "Spanish",
"link": "https://en.wikipedia.org/wiki/Gypsy_Ballads\n",
"pages": 218,
@@ -550,9 +550,9 @@
"title": "One Hundred Years of Solitude",
"price": 490,
"year": 1967,
- "author": "Gabriel García Márquez",
+ "author": "Gabriel Garc\u00eda M\u00e1rquez",
"country": "Colombia",
- "imageLink": "images/one-hundred-years-of-solitude.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+40",
"language": "Spanish",
"link": "https://en.wikipedia.org/wiki/One_Hundred_Years_of_Solitude\n",
"pages": 417,
@@ -564,9 +564,9 @@
"title": "Love in the Time of Cholera",
"price": 500,
"year": 1985,
- "author": "Gabriel García Márquez",
+ "author": "Gabriel Garc\u00eda M\u00e1rquez",
"country": "Colombia",
- "imageLink": "images/love-in-the-time-of-cholera.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+41",
"language": "Spanish",
"link": "https://en.wikipedia.org/wiki/Love_in_the_Time_of_Cholera\n",
"pages": 368,
@@ -580,7 +580,7 @@
"year": 1832,
"author": "Johann Wolfgang von Goethe",
"country": "Saxe-Weimar",
- "imageLink": "images/faust.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+42",
"language": "German",
"link": "https://en.wikipedia.org/wiki/Goethe%27s_Faust\n",
"pages": 158,
@@ -594,7 +594,7 @@
"year": 1842,
"author": "Nikolai Gogol",
"country": "Russia",
- "imageLink": "images/dead-souls.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+43",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/Dead_Souls\n",
"pages": 432,
@@ -606,9 +606,9 @@
"title": "The Tin Drum",
"price": 530,
"year": 1959,
- "author": "Günter Grass",
+ "author": "G\u00fcnter Grass",
"country": "Germany",
- "imageLink": "images/the-tin-drum.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+44",
"language": "German",
"link": "https://en.wikipedia.org/wiki/The_Tin_Drum\n",
"pages": 600,
@@ -620,9 +620,9 @@
"title": "The Devil to Pay in the Backlands",
"price": 540,
"year": 1956,
- "author": "João Guimarães Rosa",
+ "author": "Jo\u00e3o Guimar\u00e3es Rosa",
"country": "Brazil",
- "imageLink": "images/the-devil-to-pay-in-the-backlands.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+45",
"language": "Portuguese",
"link": "https://en.wikipedia.org/wiki/The_Devil_to_Pay_in_the_Backlands\n",
"pages": 494,
@@ -636,7 +636,7 @@
"year": 1890,
"author": "Knut Hamsun",
"country": "Norway",
- "imageLink": "images/hunger.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+46",
"language": "Norwegian",
"link": "https://en.wikipedia.org/wiki/Hunger_(Hamsun_novel)\n",
"pages": 176,
@@ -650,7 +650,7 @@
"year": 1952,
"author": "Ernest Hemingway",
"country": "United States",
- "imageLink": "images/the-old-man-and-the-sea.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+47",
"language": "English",
"link": "https://en.wikipedia.org/wiki/The_Old_Man_and_the_Sea\n",
"pages": 128,
@@ -664,7 +664,7 @@
"year": -735,
"author": "Homer",
"country": "Greece",
- "imageLink": "images/the-iliad-of-homer.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+48",
"language": "Greek",
"link": "https://en.wikipedia.org/wiki/Iliad\n",
"pages": 608,
@@ -678,7 +678,7 @@
"year": -800,
"author": "Homer",
"country": "Greece",
- "imageLink": "images/the-odyssey-of-homer.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+49",
"language": "Greek",
"link": "https://en.wikipedia.org/wiki/Odyssey\n",
"pages": 374,
@@ -692,7 +692,7 @@
"year": 1879,
"author": "Henrik Ibsen",
"country": "Norway",
- "imageLink": "images/a-Dolls-house.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+50",
"language": "Norwegian",
"link": "https://en.wikipedia.org/wiki/A_Doll%27s_House\n",
"pages": 68,
@@ -706,7 +706,7 @@
"year": 1922,
"author": "James Joyce",
"country": "Irish Free State",
- "imageLink": "images/ulysses.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+51",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Ulysses_(novel)\n",
"pages": 228,
@@ -720,7 +720,7 @@
"year": 1924,
"author": "Franz Kafka",
"country": "Czechoslovakia",
- "imageLink": "images/stories-of-franz-kafka.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+52",
"language": "German",
"link": "https://en.wikipedia.org/wiki/Franz_Kafka_bibliography#Short_stories\n",
"pages": 488,
@@ -734,7 +734,7 @@
"year": 1925,
"author": "Franz Kafka",
"country": "Czechoslovakia",
- "imageLink": "images/the-trial.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+53",
"language": "German",
"link": "https://en.wikipedia.org/wiki/The_Trial\n",
"pages": 160,
@@ -748,7 +748,7 @@
"year": 1926,
"author": "Franz Kafka",
"country": "Czechoslovakia",
- "imageLink": "images/the-castle.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+54",
"language": "German",
"link": "https://en.wikipedia.org/wiki/The_Castle_(novel)\n",
"pages": 352,
@@ -760,9 +760,9 @@
"title": "The recognition of Shakuntala",
"price": 640,
"year": 150,
- "author": "Kālidāsa",
+ "author": "K\u0101lid\u0101sa",
"country": "India",
- "imageLink": "images/the-recognition-of-shakuntala.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+55",
"language": "Sanskrit",
"link": "https://en.wikipedia.org/wiki/Abhij%C3%B1%C4%81na%C5%9B%C4%81kuntalam\n",
"pages": 147,
@@ -776,7 +776,7 @@
"year": 1954,
"author": "Yasunari Kawabata",
"country": "Japan",
- "imageLink": "images/the-sound-of-the-mountain.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+56",
"language": "Japanese",
"link": "https://en.wikipedia.org/wiki/The_Sound_of_the_Mountain\n",
"pages": 288,
@@ -790,7 +790,7 @@
"year": 1946,
"author": "Nikos Kazantzakis",
"country": "Greece",
- "imageLink": "images/zorba-the-greek.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+57",
"language": "Greek",
"link": "https://en.wikipedia.org/wiki/Zorba_the_Greek\n",
"pages": 368,
@@ -804,7 +804,7 @@
"year": 1913,
"author": "D. H. Lawrence",
"country": "United Kingdom",
- "imageLink": "images/sons-and-lovers.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+58",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Sons_and_Lovers\n",
"pages": 432,
@@ -816,9 +816,9 @@
"title": "Independent People",
"price": 680,
"year": 1934,
- "author": "Halldór Laxness",
+ "author": "Halld\u00f3r Laxness",
"country": "Iceland",
- "imageLink": "images/independent-people.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+59",
"language": "Icelandic",
"link": "https://en.wikipedia.org/wiki/Independent_People\n",
"pages": 470,
@@ -832,7 +832,7 @@
"year": 1818,
"author": "Giacomo Leopardi",
"country": "Italy",
- "imageLink": "images/poems-giacomo-leopardi.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+60",
"language": "Italian",
"link": "\n",
"pages": 184,
@@ -846,7 +846,7 @@
"year": 1962,
"author": "Doris Lessing",
"country": "United Kingdom",
- "imageLink": "images/the-golden-notebook.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+61",
"language": "English",
"link": "https://en.wikipedia.org/wiki/The_Golden_Notebook\n",
"pages": 688,
@@ -860,7 +860,7 @@
"year": 1945,
"author": "Astrid Lindgren",
"country": "Sweden",
- "imageLink": "images/pippi-longstocking.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+62",
"language": "Swedish",
"link": "https://en.wikipedia.org/wiki/Pippi_Longstocking\n",
"pages": 160,
@@ -874,7 +874,7 @@
"year": 1918,
"author": "Lu Xun",
"country": "China",
- "imageLink": "images/diary-of-a-madman.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+63",
"language": "Chinese",
"link": "https://en.wikipedia.org/wiki/A_Madman%27s_Diary\n",
"pages": 389,
@@ -888,7 +888,7 @@
"year": 1959,
"author": "Naguib Mahfouz",
"country": "Egypt",
- "imageLink": "images/children-of-gebelawi.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+64",
"language": "Arabic",
"link": "https://en.wikipedia.org/wiki/Children_of_Gebelawi\n",
"pages": 355,
@@ -902,7 +902,7 @@
"year": 1901,
"author": "Thomas Mann",
"country": "Germany",
- "imageLink": "images/buddenbrooks.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+65",
"language": "German",
"link": "https://en.wikipedia.org/wiki/Buddenbrooks\n",
"pages": 736,
@@ -916,7 +916,7 @@
"year": 1924,
"author": "Thomas Mann",
"country": "Germany",
- "imageLink": "images/the-magic-mountain.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+66",
"language": "German",
"link": "https://en.wikipedia.org/wiki/The_Magic_Mountain\n",
"pages": 720,
@@ -930,7 +930,7 @@
"year": 1851,
"author": "Herman Melville",
"country": "United States",
- "imageLink": "images/moby-dick.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+67",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Moby-Dick\n",
"pages": 378,
@@ -944,7 +944,7 @@
"year": 1595,
"author": "Michel de Montaigne",
"country": "France",
- "imageLink": "images/essais.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+68",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Essays_(Montaigne)\n",
"pages": 404,
@@ -958,7 +958,7 @@
"year": 1974,
"author": "Elsa Morante",
"country": "Italy",
- "imageLink": "images/history.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+69",
"language": "Italian",
"link": "https://en.wikipedia.org/wiki/History_(novel)\n",
"pages": 600,
@@ -972,7 +972,7 @@
"year": 1987,
"author": "Toni Morrison",
"country": "United States",
- "imageLink": "images/beloved.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+70",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Beloved_(novel)\n",
"pages": 321,
@@ -986,7 +986,7 @@
"year": 1006,
"author": "Murasaki Shikibu",
"country": "Japan",
- "imageLink": "images/the-tale-of-genji.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+71",
"language": "Japanese",
"link": "https://en.wikipedia.org/wiki/The_Tale_of_Genji\n",
"pages": 1360,
@@ -1000,7 +1000,7 @@
"year": 1931,
"author": "Robert Musil",
"country": "Austria",
- "imageLink": "images/the-man-without-qualities.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+72",
"language": "German",
"link": "https://en.wikipedia.org/wiki/The_Man_Without_Qualities\n",
"pages": 365,
@@ -1014,7 +1014,7 @@
"year": 1955,
"author": "Vladimir Nabokov",
"country": "Russia/United States",
- "imageLink": "images/lolita.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+73",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Lolita\n",
"pages": 317,
@@ -1028,7 +1028,7 @@
"year": 1949,
"author": "George Orwell",
"country": "United Kingdom",
- "imageLink": "images/nineteen-eighty-four.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+74",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Nineteen_Eighty-Four\n",
"pages": 272,
@@ -1042,7 +1042,7 @@
"year": 100,
"author": "Ovid",
"country": "Roman Empire",
- "imageLink": "images/the-metamorphoses-of-ovid.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+75",
"language": "Classical Latin",
"link": "https://en.wikipedia.org/wiki/Metamorphoses\n",
"pages": 576,
@@ -1056,7 +1056,7 @@
"year": 1928,
"author": "Fernando Pessoa",
"country": "Portugal",
- "imageLink": "images/the-book-of-disquiet.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+76",
"language": "Portuguese",
"link": "https://en.wikipedia.org/wiki/The_Book_of_Disquiet\n",
"pages": 272,
@@ -1070,7 +1070,7 @@
"year": 1950,
"author": "Edgar Allan Poe",
"country": "United States",
- "imageLink": "images/tales-and-poems-of-edgar-allan-poe.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+77",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Edgar_Allan_Poe_bibliography#Tales\n",
"pages": 842,
@@ -1084,7 +1084,7 @@
"year": 1920,
"author": "Marcel Proust",
"country": "France",
- "imageLink": "images/a-la-recherche-du-temps-perdu.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+78",
"language": "French",
"link": "https://en.wikipedia.org/wiki/In_Search_of_Lost_Time\n",
"pages": 2408,
@@ -1096,9 +1096,9 @@
"title": "Gargantua and Pantagruel",
"price": 880,
"year": 1533,
- "author": "François Rabelais",
+ "author": "Fran\u00e7ois Rabelais",
"country": "France",
- "imageLink": "images/gargantua-and-pantagruel.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+79",
"language": "French",
"link": "https://en.wikipedia.org/wiki/Gargantua_and_Pantagruel\n",
"pages": 623,
@@ -1107,12 +1107,12 @@
},
{
"id": 80,
- "title": "Pedro Páramo",
+ "title": "Pedro P\u00e1ramo",
"price": 890,
"year": 1955,
"author": "Juan Rulfo",
"country": "Mexico",
- "imageLink": "images/pedro-paramo.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+80",
"language": "Spanish",
"link": "https://en.wikipedia.org/wiki/Pedro_P%C3%A1ramo\n",
"pages": 124,
@@ -1126,7 +1126,7 @@
"year": 1236,
"author": "Rumi",
"country": "Sultanate of Rum",
- "imageLink": "images/the-masnavi.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+81",
"language": "Persian",
"link": "https://en.wikipedia.org/wiki/Masnavi\n",
"pages": 438,
@@ -1140,7 +1140,7 @@
"year": 1981,
"author": "Salman Rushdie",
"country": "United Kingdom, India",
- "imageLink": "images/midnights-children.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+82",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Midnight%27s_Children\n",
"pages": 536,
@@ -1154,7 +1154,7 @@
"year": 1257,
"author": "Saadi",
"country": "Persia, Persian Empire",
- "imageLink": "images/bostan.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+83",
"language": "Persian",
"link": "https://en.wikipedia.org/wiki/Bustan_(book)\n",
"pages": 298,
@@ -1168,7 +1168,7 @@
"year": 1966,
"author": "Tayeb Salih",
"country": "Sudan",
- "imageLink": "images/season-of-migration-to-the-north.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+84",
"language": "Arabic",
"link": "https://en.wikipedia.org/wiki/Season_of_Migration_to_the_North\n",
"pages": 139,
@@ -1180,9 +1180,9 @@
"title": "Blindness",
"price": 940,
"year": 1995,
- "author": "José Saramago",
+ "author": "Jos\u00e9 Saramago",
"country": "Portugal",
- "imageLink": "images/blindness.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+85",
"language": "Portuguese",
"link": "https://en.wikipedia.org/wiki/Blindness_(novel)\n",
"pages": 352,
@@ -1196,7 +1196,7 @@
"year": 1603,
"author": "William Shakespeare",
"country": "England",
- "imageLink": "images/hamlet.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+86",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Hamlet\n",
"pages": 432,
@@ -1210,7 +1210,7 @@
"year": 1608,
"author": "William Shakespeare",
"country": "England",
- "imageLink": "images/king-lear.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+87",
"language": "English",
"link": "https://en.wikipedia.org/wiki/King_Lear\n",
"pages": 384,
@@ -1224,7 +1224,7 @@
"year": 1609,
"author": "William Shakespeare",
"country": "England",
- "imageLink": "images/othello.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+88",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Othello\n",
"pages": 314,
@@ -1238,7 +1238,7 @@
"year": -430,
"author": "Sophocles",
"country": "Greece",
- "imageLink": "images/oedipus-the-king.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+89",
"language": "Greek",
"link": "https://en.wikipedia.org/wiki/Oedipus_the_King\n",
"pages": 88,
@@ -1252,7 +1252,7 @@
"year": 1830,
"author": "Stendhal",
"country": "France",
- "imageLink": "images/le-rouge-et-le-noir.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+90",
"language": "French",
"link": "https://en.wikipedia.org/wiki/The_Red_and_the_Black\n",
"pages": 576,
@@ -1266,7 +1266,7 @@
"year": 1760,
"author": "Laurence Sterne",
"country": "England",
- "imageLink": "images/the-life-and-opinions-of-tristram-shandy.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+91",
"language": "English",
"link": "https://en.wikipedia.org/wiki/The_Life_and_Opinions_of_Tristram_Shandy,_Gentleman\n",
"pages": 640,
@@ -1280,7 +1280,7 @@
"year": 1923,
"author": "Italo Svevo",
"country": "Italy",
- "imageLink": "images/confessions-of-zeno.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+92",
"language": "Italian",
"link": "https://en.wikipedia.org/wiki/Zeno%27s_Conscience\n",
"pages": 412,
@@ -1294,7 +1294,7 @@
"year": 1726,
"author": "Jonathan Swift",
"country": "Ireland",
- "imageLink": "images/gullivers-travels.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+93",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Gulliver%27s_Travels\n",
"pages": 178,
@@ -1308,7 +1308,7 @@
"year": 1867,
"author": "Leo Tolstoy",
"country": "Russia",
- "imageLink": "images/war-and-peace.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+94",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/War_and_Peace\n",
"pages": 1296,
@@ -1322,7 +1322,7 @@
"year": 1877,
"author": "Leo Tolstoy",
"country": "Russia",
- "imageLink": "images/anna-karenina.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+95",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/Anna_Karenina\n",
"pages": 864,
@@ -1336,7 +1336,7 @@
"year": 1886,
"author": "Leo Tolstoy",
"country": "Russia",
- "imageLink": "images/the-death-of-ivan-ilyich.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+96",
"language": "Russian",
"link": "https://en.wikipedia.org/wiki/The_Death_of_Ivan_Ilyich\n",
"pages": 92,
@@ -1350,7 +1350,7 @@
"year": 1884,
"author": "Mark Twain",
"country": "United States",
- "imageLink": "images/the-adventures-of-huckleberry-finn.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+97",
"language": "English",
"link": "https://en.wikipedia.org/wiki/Adventures_of_Huckleberry_Finn\n",
"pages": 224,
@@ -1364,7 +1364,7 @@
"year": -450,
"author": "Valmiki",
"country": "India",
- "imageLink": "images/ramayana.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+98",
"language": "Sanskrit",
"link": "https://en.wikipedia.org/wiki/Ramayana\n",
"pages": 152,
@@ -1378,7 +1378,7 @@
"year": -23,
"author": "Virgil",
"country": "Roman Empire",
- "imageLink": "images/the-aeneid.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+99",
"language": "Classical Latin",
"link": "https://en.wikipedia.org/wiki/Aeneid\n",
"pages": 442,
@@ -1392,67 +1392,11 @@
"year": -700,
"author": "Vyasa",
"country": "India",
- "imageLink": "images/the-mahab-harata.jpg",
+ "imageLink": "https://placehold.co/300x400/667eea/white?text=Book+100",
"language": "Sanskrit",
"link": "https://en.wikipedia.org/wiki/Mahabharata\n",
"pages": 276,
"createdAt": "2025-07-25T17:31:54.239043+02:00",
"updatedAt": "2025-07-25T17:31:54.239043+02:00"
- },
- {
- "id": 101,
- "title": "Leaves of Grass",
- "price": 1100,
- "year": 1855,
- "author": "Walt Whitman",
- "country": "United States",
- "imageLink": "images/leaves-of-grass.jpg",
- "language": "English",
- "link": "https://en.wikipedia.org/wiki/Leaves_of_Grass\n",
- "pages": 152,
- "createdAt": "2025-07-25T17:31:54.239043+02:00",
- "updatedAt": "2025-07-25T17:31:54.239043+02:00"
- },
- {
- "id": 102,
- "title": "Mrs Dalloway",
- "price": 1110,
- "year": 1925,
- "author": "Virginia Woolf",
- "country": "United Kingdom",
- "imageLink": "images/mrs-dalloway.jpg",
- "language": "English",
- "link": "https://en.wikipedia.org/wiki/Mrs_Dalloway\n",
- "pages": 216,
- "createdAt": "2025-07-25T17:31:54.239043+02:00",
- "updatedAt": "2025-07-25T17:31:54.239043+02:00"
- },
- {
- "id": 103,
- "title": "To the Lighthouse",
- "price": 1120,
- "year": 1927,
- "author": "Virginia Woolf",
- "country": "United Kingdom",
- "imageLink": "images/to-the-lighthouse.jpg",
- "language": "English",
- "link": "https://en.wikipedia.org/wiki/To_the_Lighthouse\n",
- "pages": 209,
- "createdAt": "2025-07-25T17:31:54.239044+02:00",
- "updatedAt": "2025-07-25T17:31:54.239044+02:00"
- },
- {
- "id": 104,
- "title": "Memoirs of Hadrian",
- "price": 1130,
- "year": 1951,
- "author": "Marguerite Yourcenar",
- "country": "France/Belgium",
- "imageLink": "images/memoirs-of-hadrian.jpg",
- "language": "French",
- "link": "https://en.wikipedia.org/wiki/Memoirs_of_Hadrian\n",
- "pages": 408,
- "createdAt": "2025-07-25T17:31:54.239044+02:00",
- "updatedAt": "2025-07-25T17:31:54.239044+02:00"
}
]
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 0e6860b..2668a7e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,37 +1,31 @@
module github.com/jkaninda/okapi-example
-go 1.24.5
+go 1.25.5
require (
github.com/golang-jwt/jwt/v5 v5.3.0
+ github.com/jkaninda/go-utils v0.1.4
github.com/jkaninda/logger v0.0.5
- github.com/jkaninda/okapi v0.0.18
+ github.com/jkaninda/okapi v0.2.0
+ github.com/jkaninda/okapi-ws v0.0.0-20260117135650-dd9a12fcc236
+ github.com/joho/godotenv v1.5.1
)
require (
- github.com/KyleBanks/depth v1.2.1 // indirect
- github.com/getkin/kin-openapi v0.132.0 // indirect
- github.com/go-openapi/jsonpointer v0.21.1 // indirect
- github.com/go-openapi/jsonreference v0.21.0 // indirect
- github.com/go-openapi/spec v0.21.0 // indirect
- github.com/go-openapi/swag v0.23.1 // indirect
+ github.com/getkin/kin-openapi v0.133.0 // indirect
+ github.com/go-openapi/jsonpointer v0.22.4 // indirect
+ github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
- github.com/jkaninda/go-utils v0.1.1 // indirect
+ github.com/gorilla/websocket v1.5.3 // indirect
github.com/josharian/intern v1.0.0 // indirect
- github.com/mailru/easyjson v0.9.0 // indirect
+ github.com/mailru/easyjson v0.9.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
- github.com/swaggo/files v1.0.1 // indirect
- github.com/swaggo/http-swagger v1.3.4 // indirect
- github.com/swaggo/swag v1.16.6 // indirect
- golang.org/x/mod v0.27.0 // indirect
- golang.org/x/net v0.43.0 // indirect
- golang.org/x/sync v0.16.0 // indirect
- golang.org/x/tools v0.36.0 // indirect
- google.golang.org/protobuf v1.36.7 // indirect
+ github.com/woodsbury/decimal128 v1.4.0 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 675d97f..365cd3d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,53 +1,43 @@
-github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
-github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
-github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
-github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
-github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
-github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
-github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
-github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
-github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
-github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
-github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
-github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
-github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
-github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
+github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
+github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
+github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
+github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
+github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
+github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
+github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
+github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
+github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/jkaninda/go-utils v0.1.1 h1:PMrtXR9d51YzHo85y9Z6YVL0YyBURbRTPemHVbFDqZg=
-github.com/jkaninda/go-utils v0.1.1/go.mod h1:pf0/U6k4JbxlablM2G4eSTZdQ2LFshfAsCK5Q8qNfGo=
-github.com/jkaninda/logger v0.0.3 h1:WaGem831HfeSE//ZZKuJTFv9Jaof9xyAH2PArT5tKps=
-github.com/jkaninda/logger v0.0.3/go.mod h1:ZUXJ2BdxDPG6e8t6mbKhc2ZFaFi2Iuy/4ukquFrPkFE=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/jkaninda/go-utils v0.1.4 h1:ZdNlI+yLWc4/S0qKcCNQIPj+6lHSdJcGaxtRADAifAU=
+github.com/jkaninda/go-utils v0.1.4/go.mod h1:Aa54jEAcDykc3CnOdreqZG80UfSZOvrYecyusu+oPb4=
github.com/jkaninda/logger v0.0.5 h1:fTHKgDsHtuN8rkSBvwe6QStfi8yIdm8O3r0g99dRDTY=
github.com/jkaninda/logger v0.0.5/go.mod h1:ZUXJ2BdxDPG6e8t6mbKhc2ZFaFi2Iuy/4ukquFrPkFE=
-github.com/jkaninda/okapi v0.0.3 h1:Bxceqe/4mUIQR+YvGWOC5h3io/PX56Uj0jNWaeh9A/g=
-github.com/jkaninda/okapi v0.0.3/go.mod h1:be2CsFhhvLh7sQ0d+4/1P/IbAyYB32i0sJpdfeS+wWY=
-github.com/jkaninda/okapi v0.0.5 h1:tdpDvOSw8dR4b1yBGUo1do6mrv6y0d+hEFev447MqPY=
-github.com/jkaninda/okapi v0.0.5/go.mod h1:AR5cmx3muUi2hkMZTsA2FJ7ABfKKfMttx2h1Dv+D6AM=
-github.com/jkaninda/okapi v0.0.6 h1:usUuZMB/bZzPM9dgb21GhyqC3TTSr+qgH/DfdTPh+NM=
-github.com/jkaninda/okapi v0.0.6/go.mod h1:AR5cmx3muUi2hkMZTsA2FJ7ABfKKfMttx2h1Dv+D6AM=
-github.com/jkaninda/okapi v0.0.7 h1:jFPDfTR9BEKWoLnSdZxfmSyOp4EetXCKjw+uqurTKXI=
-github.com/jkaninda/okapi v0.0.7/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY=
-github.com/jkaninda/okapi v0.0.8 h1:gEdFRgueaRXEv4aZzk62p98xqctyLHIYC5u8R1jBNoY=
-github.com/jkaninda/okapi v0.0.8/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY=
-github.com/jkaninda/okapi v0.0.10 h1:uqgz38jd1hanhIa2GMW7IsxiL6nWAdWgWF81yEN4WBc=
-github.com/jkaninda/okapi v0.0.10/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY=
-github.com/jkaninda/okapi v0.0.11 h1:JglVjhJq4NzXrAftm1IdTW9m4jxmitKUq9767Gf8d5A=
-github.com/jkaninda/okapi v0.0.11/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY=
-github.com/jkaninda/okapi v0.0.14 h1:6MP9AbTqpVSPIDunt7iC/uK0jXEmEZ7fiExTwEgFRgo=
-github.com/jkaninda/okapi v0.0.14/go.mod h1:EnmxlW7HZLH5tYMoG8gpub+1S5YUPwEnVo97q45GcrY=
-github.com/jkaninda/okapi v0.0.17 h1:zGap8pLwdbjBPF1ws8Bcgoo0/3CLXTv+NZnUrpz/XAc=
-github.com/jkaninda/okapi v0.0.17/go.mod h1:lGcmGDxXBE4UDOKjNAnveUIxdG4Ddnc0BByI+hL/vZs=
-github.com/jkaninda/okapi v0.0.18 h1:7wfidIPpSH8B3fRqZu5+IEnjuGKZq/69Rn99NcQgFrM=
-github.com/jkaninda/okapi v0.0.18/go.mod h1:X5rrqoVA+6si4kFhr8/rkWjHSHgG0C/TsyJwZfoogho=
+github.com/jkaninda/okapi v0.1.3-rc1 h1:ClpTUEIRmgQmHKprecka2GboN06gj/4MyxXz2bKSwFs=
+github.com/jkaninda/okapi v0.1.3-rc1/go.mod h1:VORIE1RY2jhNsodcHF2kLNjAXwp4G7EBXf//VRZP2lc=
+github.com/jkaninda/okapi v0.1.3-rc2 h1:D0INvElPvRKqVZ+VXkowmq4ZnGsk8K7ZTLpyzG/7BkA=
+github.com/jkaninda/okapi v0.1.3-rc2/go.mod h1:VORIE1RY2jhNsodcHF2kLNjAXwp4G7EBXf//VRZP2lc=
+github.com/jkaninda/okapi v0.2.0 h1:sWWysiEvuxqXMbEwjDpY1CTsgIBs130p8LX+7IISeTE=
+github.com/jkaninda/okapi v0.2.0/go.mod h1:VORIE1RY2jhNsodcHF2kLNjAXwp4G7EBXf//VRZP2lc=
+github.com/jkaninda/okapi-ws v0.0.0-20260117135650-dd9a12fcc236 h1:hs0ybM54mAR9wVeDM32CAvNwT+LEGjP098AHlmsEV3g=
+github.com/jkaninda/okapi-ws v0.0.0-20260117135650-dd9a12fcc236/go.mod h1:wC/XqK1Ti05kWx+aZ3wfB2Q8zDi/drg9Lpvsq06gksY=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
-github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
+github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
+github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
@@ -56,65 +46,17 @@ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletI
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
-github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
-github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
-github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
-github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
-github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
-github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
-github.com/swaggo/swag v1.16.5 h1:nMf2fEV1TetMTJb4XzD0Lz7jFfKJmJKGTygEey8NSxM=
-github.com/swaggo/swag v1.16.5/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
-github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
-github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
-golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
-golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
-golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
-golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
-golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
-golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
-golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
-golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
-golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
-golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
-golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
-golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
-google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
-google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
-google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
+github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
diff --git a/home-page.png b/home-page.png
new file mode 100644
index 0000000..1efc6b4
Binary files /dev/null and b/home-page.png differ
diff --git a/main.go b/main.go
index 1bcb4d0..dc7fcbc 100644
--- a/main.go
+++ b/main.go
@@ -1,27 +1,39 @@
package main
import (
+ "embed"
+ "html/template"
+ "io"
+
+ "github.com/jkaninda/logger"
"github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi-example/config"
"github.com/jkaninda/okapi-example/routes"
)
+//go:embed views/*
+var Views embed.FS
+
+type Template struct {
+ templates *template.Template
+}
+
+func (t *Template) Render(w io.Writer, name string, data interface{}, c *okapi.Context) error {
+ return t.templates.ExecuteTemplate(w, name, data)
+}
+func NewTemplate() *Template {
+ tmpl := template.Must(template.ParseFS(Views, "views/*.html"))
+ return &Template{templates: tmpl}
+}
func main() {
- // Create a new Okapi instance with default config
app := okapi.New()
- route := routes.NewRoute(app)
-
- // ************ Registering Routes ************
- // Register home route
- app.Register(route.Home())
- app.Register(route.WhoAmI())
- // Auth
- app.Register(route.AuthRoute())
- // Register book routes
- app.Register(route.BookRoutes()...)
- app.Register(route.APIBookRoutes()...)
- app.Register(route.CommonRoutes()...)
- // Admin routes
- app.Register(route.AdminRoutes()...)
+ conf := config.New()
+ if err := conf.Initialize(app); err != nil {
+ logger.Fatal("Failed to initialize config", "error", err)
+ }
+ app.WithRenderer(NewTemplate())
+ route := routes.New(app, conf)
+ route.RegisterRoutes()
// Start the server
if err := app.Start(); err != nil {
diff --git a/middlewares/middleware.go b/middlewares/middleware.go
index 2a23c6d..25feb17 100644
--- a/middlewares/middleware.go
+++ b/middlewares/middleware.go
@@ -2,20 +2,21 @@ package middlewares
import (
"fmt"
- "github.com/jkaninda/logger"
- "github.com/jkaninda/okapi-example/utils"
"log/slog"
"net/http"
"strings"
"time"
+ goutils "github.com/jkaninda/go-utils"
+ "github.com/jkaninda/logger"
+
"github.com/golang-jwt/jwt/v5"
"github.com/jkaninda/okapi"
"github.com/jkaninda/okapi-example/models"
)
var (
- signingSecret = utils.GetSingingSecret()
+ signingSecret = goutils.Env("JWT_SECRET", "default-secret-key")
JWTAuth = &okapi.JWTAuth{
SigningSecret: []byte(signingSecret),
Audience: "okapi.jkaninda.dev",
@@ -40,12 +41,10 @@ var (
"name": "user.name",
},
// CustomClaims claims validation function
- ValidateClaims: func(context okapi.Context, claims jwt.Claims) error {
+ ValidateClaims: func(context *okapi.Context, claims jwt.Claims) error {
slog.Info("Validating JWT claims for role using custom function")
// Simulate a custom claims validation
- if _, ok := claims.(jwt.Claims); ok {
- }
return nil
},
}
@@ -65,17 +64,14 @@ var (
adminPermissions = []string{"read", "create", "delete", "update"}
)
-func Login(authRequest *models.AuthRequest) (models.AuthResponse, error) {
+func Login(authRequest *models.AuthRequest) (models.Auth, error) {
// This is where you would typically validate the user credentials against a database
logger.Info("Login attempt", "username", authRequest.Username)
// Simulate a login function that returns a JWT token
if authRequest.Username != "admin" && authRequest.Password != "password" ||
authRequest.Username != "user" && authRequest.Password != "password" {
- return models.AuthResponse{
- Success: false,
- Message: "Invalid username or password",
- }, fmt.Errorf("username or password is wrong")
+ return models.Auth{}, fmt.Errorf("username or password is wrong")
}
if _, ok := jwtClaims["user"].(map[string]string); ok {
@@ -97,21 +93,21 @@ func Login(authRequest *models.AuthRequest) (models.AuthResponse, error) {
token, err := okapi.GenerateJwtToken(JWTAuth.SigningSecret, jwtClaims, expireAt)
if err != nil {
- return models.AuthResponse{
- Success: false,
- Message: "Invalid username or password",
- }, fmt.Errorf("failed to generate JWT token: %w", err)
+ return models.Auth{}, fmt.Errorf("failed to generate JWT token: %w", err)
}
- return models.AuthResponse{
- Success: true,
- Message: "Welcome back " + authRequest.Username,
+ return models.Auth{
Token: token,
ExpiresAt: time.Now().Add(expireAt).Unix(),
+ USer: models.UserInfo{
+ Name: strings.ToTitle(authRequest.Username),
+ Role: authRequest.Username,
+ Email: fmt.Sprintf("%s@example.com", authRequest.Username),
+ },
}, nil
}
-func CustomMiddleware(next okapi.HandleFunc) okapi.HandleFunc {
- return func(c okapi.Context) error {
+func CustomMiddleware(next okapi.HandlerFunc) okapi.HandlerFunc {
+ return func(c *okapi.Context) error {
slog.Info("Custom middleware executed", "path", c.Request().URL.Path, "method", c.Request().Method)
// You can add any custom logic here, such as logging, authentication, etc.
// For example, let's log the request method and URL
diff --git a/models/model.go b/models/model.go
index 95ec9df..90104fd 100644
--- a/models/model.go
+++ b/models/model.go
@@ -27,12 +27,46 @@ package models
import "time"
// **************** Models ************************
-
-type Response struct {
+type ResponseDto struct {
Success bool `json:"success"`
Message string `json:"message"`
- Data Book `json:"data"`
+ Details any `json:"details,omitempty"`
+}
+
+type Response[T any] struct {
+ ResponseDto
+ Data T `json:"data,omitempty"`
+}
+
+func SuccessResponse[T any](message string, data T) Response[T] {
+ return Response[T]{
+ ResponseDto: ResponseDto{
+ Success: true,
+ Message: message,
+ },
+ Data: data,
+ }
+}
+func ErrorResponse(message string, err error) Response[any] {
+ return Response[any]{
+ ResponseDto: ResponseDto{
+ Success: false,
+ Message: message,
+ Details: err.Error(),
+ },
+ }
+}
+func ErrorResponseData(message string, err error, data any) Response[any] {
+ return Response[any]{
+ ResponseDto: ResponseDto{
+ Success: false,
+ Message: message,
+ Details: err,
+ },
+ Data: data,
+ }
}
+
type Book struct {
Id int `json:"id"`
Title string `json:"title" form:"title" max:"50" required:"true" description:"Book name"`
@@ -47,21 +81,15 @@ type Book struct {
CreatedAt time.Time `json:"createdAt" form:"createdAt" query:"createdAt" yaml:"createdAt" required:"false" description:"Book creation date"`
UpdatedAt time.Time `json:"updatedAt" form:"updatedAt" query:"updatedAt" yaml:"updatedAt" required:"false" description:"Book last update date"`
}
-type ErrorResponse struct {
- Success bool `json:"success"`
- Status int `json:"status"`
- Details any `json:"details"`
-}
type AuthRequest struct {
- Username string `json:"username" required:"true" description:"Username for authentication"`
+ Username string `json:"username" required:"true" description:"Username for authentication" minLength:"3" pattern:"^[a-z]+$"`
Password string `json:"password" required:"true" description:"Password for authentication"`
}
-type AuthResponse struct {
- Success bool `json:"success"`
- Message string `json:"message"`
- Token string `json:"token,omitempty"`
- ExpiresAt int64 `json:"expires,omitempty"`
+type Auth struct {
+ Token string `json:"token,omitempty"`
+ ExpiresAt int64 `json:"expires,omitempty"`
+ USer UserInfo `json:"user"`
}
type UserInfo struct {
Name string `json:"name"`
@@ -74,3 +102,23 @@ type WhoAmIResponse struct {
RealIp string `json:"realIp"`
CurrentUser UserInfo `json:"currentUser"`
}
+type WhoAmIRequest struct {
+ Email string `header:"current_user_email" format:"email"`
+ Name string `header:"current_user_name"`
+ Role string `header:"current_user_role" enum:"ADMIN,admin,USER,user" doc:"One of ADMIN,admin,USER or user"`
+}
+type BookResponse = Response[Book]
+type UserResponse = Response[UserInfo]
+type BooksResponse = Response[[]Book]
+type AuthResponse = Response[Auth]
+
+type RequestDto struct {
+ Platform string `json:"platform" enum:"web,mobile"`
+ AppId string `json:"appId" enum:"com.mobile,com.web"`
+ Details any `json:"details,omitempty"`
+}
+
+type Request[T any] struct {
+ RequestDto
+ Fields T `json:"fields,omitempty"`
+}
diff --git a/routes/route.go b/routes/route.go
index 5bce563..3613251 100644
--- a/routes/route.go
+++ b/routes/route.go
@@ -25,18 +25,20 @@
package routes
import (
- "github.com/jkaninda/okapi-example/controllers"
- "github.com/jkaninda/okapi-example/models"
"net/http"
+ "github.com/jkaninda/okapi-example/config"
+ "github.com/jkaninda/okapi-example/models"
+ "github.com/jkaninda/okapi-example/services"
+
"github.com/jkaninda/okapi"
"github.com/jkaninda/okapi-example/middlewares"
)
var (
- bookController = &controllers.BookController{}
- homeController = &controllers.HomeController{}
- authController = &controllers.AuthController{}
+ bookService = &services.BookService{}
+ commonService = &services.CommonService{}
+ authService = &services.AuthService{}
bearerAuthSecurity = []map[string][]string{
{
"bearerAuth": {},
@@ -47,171 +49,167 @@ var (
// You can also use this example
type Route struct {
- app *okapi.Okapi
+ app *okapi.Okapi
+ group *okapi.Group
+ cfg *config.Config
}
// NewRoute creates a new Route instance with the provided Okapi app
-// NewRoute creates a new Route instance with the Okapi application
-func NewRoute(app *okapi.Okapi) *Route {
- // Update OpenAPI documentation with the application title and version
- app.WithOpenAPIDocs(okapi.OpenAPI{
- Title: "Okapi Web Framework Example",
- Version: "1.0.0",
- License: okapi.License{
- Name: "MIT",
- },
- SecuritySchemes: okapi.SecuritySchemes{
- {
- Name: "bearerAuth",
- Type: "http",
- Scheme: "bearer",
- BearerFormat: "JWT",
- },
- },
- })
+func New(app *okapi.Okapi, conf *config.Config) *Route {
+ commonService.SessionManager = conf.SessionManager
return &Route{
- app: app,
+ app: app,
+ group: &okapi.Group{Prefix: "api/v1"},
+ cfg: conf,
}
}
-// ****************** Route Definitions ******************
+func (r *Route) RegisterRoutes() {
+ r.registerAll()
+ r.app.Register(r.whoAmI())
+ r.app.Register(r.authRoute())
+ r.app.Register(r.coreRoutes()...)
+ r.app.Register(r.bookRoutes()...)
+ r.app.Register(r.v1BookRoutes()...)
+ // Admin routes
+ r.app.Register(r.AdminRoutes()...)
+}
+
+// Home return Render
+func (r *Route) registerAll() {
+ r.app.Get("/", func(c *okapi.Context) error {
+ return commonService.Home(c)
+ })
+ r.app.Get("/sse/sessions", func(c *okapi.Context) error {
+ return commonService.Session(c)
+ },
+ okapi.Summary("Get current server time"),
+ )
+ r.app.Get("/ws", commonService.WebSocketHandle,
+ okapi.Summary("Start Websocket"),
+ okapi.DocQueryParam("token", "string", "Websocket auth token", false),
+ )
-// Home returns the route definition for the HomeController
-func (r *Route) Home() okapi.RouteDefinition {
- return okapi.RouteDefinition{
- Path: "/",
- Method: http.MethodGet,
- Handler: homeController.Home,
- Group: &okapi.Group{Prefix: "/", Tags: []string{"HomeController"}},
- }
}
+// ****************** Route Definitions ******************
+
// WhoAmI returns the route definition for the HomeController
-func (r *Route) WhoAmI() okapi.RouteDefinition {
+func (r *Route) whoAmI() okapi.RouteDefinition {
return okapi.RouteDefinition{
- Path: "/whoami",
- Method: http.MethodGet,
- Handler: homeController.WhoAmI,
- Group: &okapi.Group{Prefix: "/", Tags: []string{"HomeController"}},
- Options: []okapi.RouteOption{
- okapi.DocSummary("Whoami"),
- okapi.DocDescription("Get the current user's information, no auth required"),
- okapi.DocHeader("current_user_email", "string", "current user", false),
- okapi.DocHeader("current_user_name", "string", "current name", false),
- okapi.DocHeader("current_user_role", "string", "current role", false),
- okapi.DocResponse(models.WhoAmIResponse{}),
- },
+ Path: "/whoami",
+ Method: http.MethodGet,
+ Handler: commonService.WhoAmI,
+ Group: r.group,
+ Summary: "Whoami",
+ Description: "Get the current user's information, no auth requir",
+ Request: &models.WhoAmIRequest{},
+ Response: &models.WhoAmIResponse{},
}
}
// ************* Book Routes *************
-// APIBookRoutes returns the route definitions for the BookController
-func (r *Route) APIBookRoutes() []okapi.RouteDefinition {
- apiGroup := &okapi.Group{Prefix: "/api", Tags: []string{"BookController"}}
+// bookRoutes returns the route definitions for the BookService
+func (r *Route) bookRoutes() []okapi.RouteDefinition {
+ apiGroup := &okapi.Group{Prefix: "/api", Tags: []string{"BookService"}}
apiGroup.Use(middlewares.CustomMiddleware)
apiGroup.Deprecated()
return []okapi.RouteDefinition{
{
Method: http.MethodGet,
Path: "/books",
- Handler: bookController.GetBooks,
+ Handler: bookService.List,
Group: apiGroup,
Middlewares: []okapi.Middleware{},
Options: []okapi.RouteOption{
okapi.DocSummary("Get Books"),
okapi.DocDescription("Retrieve a list of books"),
okapi.DocResponse([]models.Book{}),
- okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
+ okapi.DocResponse(&models.BookResponse{}),
},
},
{
Method: http.MethodGet,
Path: "/books/:id",
- Handler: bookController.GetBook,
+ Handler: bookService.Get,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Get Book by ID"),
okapi.DocDescription("Retrieve a book by its ID"),
okapi.DocPathParam("id", "int", "The ID of the book"),
okapi.DocResponse(models.Book{}),
- okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
- okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}),
+ okapi.DocResponse(http.StatusBadRequest, &models.Response[models.Book]{}),
},
},
}
}
-func (r *Route) BookRoutes() []okapi.RouteDefinition {
+func (r *Route) v1BookRoutes() []okapi.RouteDefinition {
+ apiGroup := r.group.Group("/books").WithTags([]string{"V1BookService"})
+
return []okapi.RouteDefinition{
{
Method: http.MethodGet,
- Path: "/books",
- Handler: bookController.GetBooks,
+ Path: "",
+ Handler: bookService.List,
Middlewares: []okapi.Middleware{middlewares.CustomMiddleware},
- Options: []okapi.RouteOption{
- okapi.DocSummary("Get Books"),
- okapi.DocDescription("Retrieve a list of books"),
- okapi.DocResponse([]models.Book{}),
- okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
- },
+ Group: apiGroup,
+ Summary: "Get Books",
+ Description: "Retrieve a list of books",
+ Response: &models.BooksResponse{},
},
{
Method: http.MethodGet,
- Path: "/books/:id",
- Handler: bookController.GetBook,
+ Path: "/:id",
+ Handler: bookService.Get,
Middlewares: []okapi.Middleware{middlewares.CustomMiddleware},
+ Group: apiGroup,
+ Summary: "Get Book by ID",
+ Description: "Retrieve a book by its ID",
Options: []okapi.RouteOption{
- okapi.DocSummary("Get Book by ID"),
- okapi.DocDescription("Retrieve a book by its ID"),
okapi.DocPathParam("id", "int", "The ID of the book"),
- okapi.DocResponse(models.Book{}),
- okapi.DocResponse(http.StatusBadRequest, models.ErrorResponse{}),
- okapi.DocResponse(http.StatusNotFound, models.ErrorResponse{}),
- },
+ okapi.DocResponse(&models.Response[models.Book]{}),
+ okapi.DocResponse(http.StatusBadRequest, &models.Response[any]{})},
},
}
}
// *************** Auth Routes ****************
-func (r *Route) AuthRoute() okapi.RouteDefinition {
- apiGroup := &okapi.Group{Prefix: "/auth", Tags: []string{"AuthController"}}
+func (r *Route) authRoute() okapi.RouteDefinition {
+ apiGroup := r.group.Group("/auth").WithTags([]string{"AuthService"})
apiGroup.Use(middlewares.CustomMiddleware)
return okapi.RouteDefinition{
- Method: http.MethodPost,
- Path: "/login",
- Handler: authController.Login,
- Group: apiGroup,
- Options: []okapi.RouteOption{
- okapi.DocSummary("Login"),
- okapi.DocDescription("User login to get a JWT token"),
- okapi.DocRequestBody(models.AuthRequest{}),
- okapi.DocResponse(models.AuthResponse{}),
- okapi.DocResponse(http.StatusUnauthorized, models.AuthResponse{}),
- },
+ Method: http.MethodPost,
+ Path: "/login",
+ Handler: authService.Login,
+ Group: apiGroup,
+ Summary: "Login",
+ Description: "User login to get a JWT token",
+ Request: &models.AuthRequest{},
+ Response: &models.AuthResponse{},
}
}
// ************** Authenticated Routes **************
-func (r *Route) CommonRoutes() []okapi.RouteDefinition {
- coreGroup := &okapi.Group{Prefix: "/core", Tags: []string{"SecurityController"}}
+func (r *Route) coreRoutes() []okapi.RouteDefinition {
+ coreGroup := r.group.Group("/core").WithTags([]string{"CoreService"})
// Apply JWT authentication middleware to the admin group
coreGroup.Use(middlewares.JWTAuth.Middleware)
coreGroup.Use(middlewares.CustomMiddleware)
- coreGroup.WithSecurity(bearerAuthSecurity) //Enable Bearer token for OpenAPI documentation
+ // Enable Bearer token for OpenAPI documentation
+ coreGroup.WithSecurity(bearerAuthSecurity)
return []okapi.RouteDefinition{
{
- Method: http.MethodPost,
- Path: "/whoami",
- Handler: authController.WhoAmI,
- Group: coreGroup,
- Options: []okapi.RouteOption{
- okapi.DocSummary("Whoami"),
- okapi.DocDescription("Get the current user's information"),
- okapi.DocResponse(models.UserInfo{}),
- },
+ Method: http.MethodPost,
+ Path: "/whoami",
+ Handler: authService.WhoAmI,
+ Group: coreGroup,
+ Summary: "Whoami",
+ Description: "Get the current user's information",
+ Response: &models.Response[models.UserInfo]{},
},
}
}
@@ -219,24 +217,23 @@ func (r *Route) CommonRoutes() []okapi.RouteDefinition {
// ***************** Admin Routes *****************
func (r *Route) AdminRoutes() []okapi.RouteDefinition {
- apiGroup := &okapi.Group{Prefix: "/admin", Tags: []string{"AdminController"}}
+ apiGroup := r.group.Group("/admin").WithTags([]string{"AdminService"})
// Apply JWT authentication middleware to the admin group
apiGroup.Use(middlewares.AdminJWTAuth.Middleware)
apiGroup.Use(middlewares.CustomMiddleware)
- apiGroup.WithBearerAuth() //Enable Bearer token for OpenAPI documentation
+ apiGroup.WithBearerAuth() // Enable Bearer token for OpenAPI documentation
return []okapi.RouteDefinition{
{
Method: http.MethodPost,
Path: "/books",
- Handler: bookController.CreateBook,
+ Handler: bookService.Create,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Create Book"),
okapi.DocDescription("Create a new book"),
okapi.DocRequestBody(models.Book{}),
- okapi.DocResponse(models.Response{}),
},
Security: bearerAuthSecurity,
},
@@ -244,7 +241,7 @@ func (r *Route) AdminRoutes() []okapi.RouteDefinition {
{
Method: http.MethodGet,
Path: "/books",
- Handler: bookController.GetBooks,
+ Handler: bookService.List,
Group: apiGroup,
Options: []okapi.RouteOption{
okapi.DocSummary("Get Books"),
diff --git a/services/auth_service.go b/services/auth_service.go
new file mode 100644
index 0000000..52c14a4
--- /dev/null
+++ b/services/auth_service.go
@@ -0,0 +1,43 @@
+package services
+
+import (
+ "fmt"
+
+ "github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi-example/middlewares"
+ "github.com/jkaninda/okapi-example/models"
+)
+
+type AuthService struct{}
+
+// ******************** AuthService *****************
+
+func (bc *AuthService) Login(c *okapi.Context) error {
+ authRequest := &models.AuthRequest{}
+ err := c.Bind(authRequest)
+ if err != nil {
+ return c.ErrorBadRequest(models.ErrorResponse("Bad Request", err))
+ }
+ // Validate the authRequest and generate a JWT token
+ authResponse, err := middlewares.Login(authRequest)
+ if err != nil {
+ return c.ErrorUnauthorized(models.ErrorResponse("Invalid username or password", err))
+ }
+ return c.OK(models.SuccessResponse("Welcome back", authResponse))
+}
+func (bc *AuthService) WhoAmI(c *okapi.Context) error {
+ // Get User Information from the context, shared by the JWT middleware using forwardClaims
+ email := c.GetString("email")
+ if email == "" {
+ return c.AbortUnauthorized("Unauthorized", fmt.Errorf("user not authenticated"))
+ }
+
+ c.Response().Header().Set("X-Okapi-User", email)
+ c.Response().Header().Set("X-Okapi-User-Name", c.GetString("name"))
+ c.Response().Header().Set("X-Okapi-Role", c.GetString("role"))
+ // Respond with the current user information
+ return c.OK(models.SuccessResponse("Ok", models.UserInfo{
+ Email: email,
+ Role: c.GetString("role"),
+ Name: c.GetString("name")}))
+}
diff --git a/services/book_service.go b/services/book_service.go
new file mode 100644
index 0000000..da725c0
--- /dev/null
+++ b/services/book_service.go
@@ -0,0 +1,78 @@
+package services
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi-example/data"
+ "github.com/jkaninda/okapi-example/models"
+)
+
+type BookService struct{}
+
+var (
+ books = []*models.Book{}
+)
+
+func (bc *BookService) List(c *okapi.Context) error {
+ q := c.Query("q")
+ books = bc.booksData()
+ if len(q) > 0 {
+ return c.OK(searchBooks(books, q))
+ }
+ return c.OK(books)
+}
+
+func (bc *BookService) Create(c *okapi.Context) error {
+ // Simulate creating a book in a database
+ book := &models.Book{}
+ err := c.Bind(book)
+ if err != nil {
+ return c.ErrorBadRequest(models.ErrorResponse)
+ }
+ book.Id = len(books) + 1
+ books = append(books, book)
+
+ return c.OK(models.SuccessResponse("Book created successfully", book))
+}
+func (bc *BookService) Get(c *okapi.Context) error {
+ id := c.Param("id")
+ i, err := strconv.Atoi(id)
+ if err != nil {
+ return c.ErrorBadRequest(models.ErrorResponse("Bad Request", err))
+ }
+ // Simulate a fetching book from a database
+
+ books = bc.booksData()
+
+ for _, book := range books {
+ if book.Id == i {
+ return c.OK(book)
+ }
+ }
+ return c.AbortNotFound("Book not found")
+}
+func (bc *BookService) booksData() []*models.Book {
+ books, _ = data.Books()
+ return books
+}
+func searchBooks(books []*models.Book, query string) []*models.Book {
+ if query == "" {
+ return books
+ }
+
+ query = strings.ToLower(strings.TrimSpace(query))
+ var results []*models.Book
+
+ for _, book := range books {
+ titleMatch := strings.Contains(strings.ToLower(book.Title), query)
+ authorMatch := strings.Contains(strings.ToLower(book.Author), query)
+
+ if titleMatch || authorMatch {
+ results = append(results, book)
+ }
+ }
+
+ return results
+}
diff --git a/services/book_service_test.go b/services/book_service_test.go
new file mode 100644
index 0000000..621b3e8
--- /dev/null
+++ b/services/book_service_test.go
@@ -0,0 +1,26 @@
+package services
+
+import (
+ "testing"
+
+ "github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi/okapitest"
+)
+
+var bookService = &BookService{}
+
+func TestBooksAPI(t *testing.T) {
+ // Setup test server
+ server := okapi.NewTestServer(t)
+
+ server.Get("/v1/books", bookService.List)
+ server.Get("/v1/books/{id:int}", bookService.Get)
+
+ // Create reusable client
+ client := okapitest.NewClient(t, server.BaseURL)
+
+ client.GET("/v1/books").ExpectStatusOK().ExpectBodyContains("The Kubernetes Bible")
+ client.GET("/v1/books?q=Vol2").ExpectStatusOK().ExpectBodyContains("System Design Interview Vol2")
+ client.GET("/v1/books/4").ExpectStatusOK().ExpectBodyContains("System Design Interview Vol2")
+
+}
diff --git a/services/common_service.go b/services/common_service.go
new file mode 100644
index 0000000..668060f
--- /dev/null
+++ b/services/common_service.go
@@ -0,0 +1,152 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Jonas Kaninda
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package services
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "time"
+
+ goutils "github.com/jkaninda/go-utils"
+ "github.com/jkaninda/logger"
+ "github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi-example/models"
+ "github.com/jkaninda/okapi-example/session"
+ okapiws "github.com/jkaninda/okapi-ws"
+)
+
+type CommonService struct {
+ SessionManager *session.SessionManager
+}
+
+// WebSocket upgrades the HTTP connection to WebSocket.
+// Config is optional; pass nil to use default settings.
+func WebSocket(config *okapiws.WSConfig, c *okapi.Context) (*okapiws.WSConnection, error) {
+ upgrader := okapiws.NewWSUpgrader(config)
+ return upgrader.Upgrade(c.Response(), c.Request(), nil)
+}
+
+// WebSocketWithHeaders upgrades with additional response headers.
+func WebSocketWithHeaders(config *okapiws.WSConfig, headers http.Header, c *okapi.Context) (*okapiws.WSConnection, error) {
+ upgrader := okapiws.NewWSUpgrader(config)
+ return upgrader.Upgrade(c.Response(), c.Request(), headers)
+}
+
+// ****************** CommonService *****************
+
+func (cs *CommonService) Home(c *okapi.Context) error {
+ return c.Render(http.StatusOK, "home", okapi.M{
+ "title": "Go Okapi basic implementation example ",
+ "message": "Hello from Okapi!",
+ "appName": "Go Okapi Bookstore",
+ "headline": "Discover your next great read",
+ "books": books,
+ })
+}
+func (cs *CommonService) Session(c *okapi.Context) error {
+ // Generate unique session ID
+ sessionID := fmt.Sprintf("session:%s-%s-%s", c.Request().RemoteAddr, goutils.Slug(c.Request().UserAgent()), time.Now().Format("20060102150405"))
+
+ // Add session
+ cs.SessionManager.AddSession(sessionID)
+ defer cs.SessionManager.RemoveSession(sessionID)
+
+ connectedAt := time.Now()
+ ticker := time.NewTicker(1 * time.Second)
+ defer ticker.Stop()
+
+ ctx := c.Request().Context()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ case t := <-ticker.C:
+ data := okapi.M{
+ "time": t.Format("15:04:05"),
+ "connectedSince": goutils.FormatDuration(time.Since(connectedAt), 1),
+ "activeConnections": cs.SessionManager.GetCount(),
+ "totalSessions": cs.SessionManager.GetTotalCount(),
+ "timestamp": t.Unix(),
+ }
+
+ if err := c.SSEvent("message", data); err != nil {
+ return err
+ }
+ }
+ }
+}
+
+func (cs *CommonService) WhoAmI(c *okapi.Context) error {
+ wr := &models.WhoAmIRequest{}
+ if err := c.Bind(wr); err != nil {
+ return c.AbortBadRequest("Bad request", err)
+ }
+ if wr.Email == "" {
+ logger.Warn("no email found")
+ }
+ return c.OK(models.WhoAmIResponse{
+ Host: c.Request().Host,
+ RealIp: c.RealIP(),
+ CurrentUser: models.UserInfo{
+ Name: wr.Name,
+ Email: wr.Email,
+ Role: wr.Role,
+ },
+ })
+}
+
+func (cs *CommonService) WebSocketHandle(c *okapi.Context) error {
+ token := c.Query("token")
+ if len(token) == 0 {
+ return c.AbortBadRequest("Bad Request", fmt.Errorf("missing token"))
+ }
+ ws, err := WebSocket(nil, c)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err := ws.Close(); err != nil {
+ log.Printf("error closing WebSocket: %v", err)
+ }
+ }()
+
+ ws.OnMessage(func(msg *okapiws.WSMessage) {
+ log.Printf("[%d] %s", msg.Type, msg.Data)
+ // Echo the message back
+ _ = ws.Send(msg.Data)
+ })
+
+ ws.OnError(func(err error) {
+ log.Printf("WebSocket error: %v", err)
+ })
+
+ ws.Start()
+
+ // Block until the connection is closed
+ <-ws.Context().Done()
+ return nil
+}
diff --git a/services/common_service_test.go b/services/common_service_test.go
new file mode 100644
index 0000000..70ef605
--- /dev/null
+++ b/services/common_service_test.go
@@ -0,0 +1,59 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2025 Jonas Kaninda
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package services
+
+import (
+ "html/template"
+ "io"
+ "testing"
+
+ "github.com/jkaninda/okapi"
+ "github.com/jkaninda/okapi-example/session"
+ "github.com/jkaninda/okapi/okapitest"
+)
+
+type Template struct {
+ templates *template.Template
+}
+
+func (t *Template) Render(w io.Writer, name string, data interface{}, c *okapi.Context) error {
+ return t.templates.ExecuteTemplate(w, name, data)
+}
+
+var commonService = &CommonService{SessionManager: session.New()}
+
+func TestCommonAPI(t *testing.T) {
+ // Setup test server
+ server := okapi.NewTestServer(t)
+ server.With().WithRenderer(&Template{templates: template.Must(template.ParseGlob("../views/*.html"))})
+
+ server.Get("/", commonService.Home)
+
+ // Create reusable client
+ client := okapitest.NewClient(t, server.BaseURL)
+
+ client.GET("/").ExpectStatusOK().ExpectBodyContains("Go Okapi Bookstore")
+
+}
diff --git a/session/sessiom_manager.go b/session/sessiom_manager.go
new file mode 100644
index 0000000..3351c1b
--- /dev/null
+++ b/session/sessiom_manager.go
@@ -0,0 +1,51 @@
+package session
+
+import (
+ "sync"
+ "time"
+)
+
+// SessionManager handles SSE session tracking
+type SessionManager struct {
+ mu sync.RWMutex
+ sessions map[string]time.Time
+ count int
+}
+
+func New() *SessionManager {
+ return &SessionManager{
+ sessions: make(map[string]time.Time),
+ }
+}
+
+// AddSession adds a new session and returns the current count
+func (sm *SessionManager) AddSession(id string) int {
+ sm.mu.Lock()
+ defer sm.mu.Unlock()
+
+ sm.sessions[id] = time.Now()
+ sm.count++
+ return sm.count
+}
+
+// RemoveSession removes a session and returns the remaining count
+func (sm *SessionManager) RemoveSession(id string) int {
+ sm.mu.Lock()
+ defer sm.mu.Unlock()
+ delete(sm.sessions, id)
+ return len(sm.sessions)
+}
+
+// GetCount returns the current active session count
+func (sm *SessionManager) GetCount() int {
+ sm.mu.RLock()
+ defer sm.mu.RUnlock()
+ return len(sm.sessions)
+}
+
+// GetTotalCount returns the total number of sessions since server start
+func (sm *SessionManager) GetTotalCount() int {
+ sm.mu.RLock()
+ defer sm.mu.RUnlock()
+ return sm.count
+}
diff --git a/utils/util.go b/utils/util.go
index 7d25860..45e36aa 100644
--- a/utils/util.go
+++ b/utils/util.go
@@ -1,11 +1,5 @@
package utils
-import "os"
+const AppName = "Okapi Web Framework Example"
-func GetSingingSecret() string {
- value := os.Getenv("JWT_SIGNING_SECRET")
- if value == "" {
- return "supersecret"
- }
- return value
-}
+var AppVersion = "1.0"
diff --git a/views/home.html b/views/home.html
new file mode 100644
index 0000000..0a7519c
--- /dev/null
+++ b/views/home.html
@@ -0,0 +1,666 @@
+{{define "home"}}
+
+
+
+
+
+ {{.title}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Connected Since
+
+
+
+
+ Loading books
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
No Books Found
+
Try adjusting your search or browse all books
+
+
+
+
+
+
+{{end}}
\ No newline at end of file