- Currency API
- Table of Contents
- Project Description
- Technology Stack
- Services
- Features
- Setup and Configuration
- API Documentation
- Auto-Generated Code
- Improvements
- Final Thoughts
This API was developed for the Bravo challenge. The goal was to create an API that could convert between various currencies, both real and fictional, with live and custom values.
Additionally, it should be noted that the API's base currency is USD.
This challenge was built using a modern, scalable technology stack:
- Go (Golang) 1.22: The core programming language, chosen for its performance, concurrency support, and robust standard library.
- PostgreSQL: A powerful, open-source relational database used for persistent storage of currency data, user information, and logs.
- Redis: An in-memory data structure store used as a caching layer to improve performance of frequent currency rate lookups.
- Docker: Used for containerization, ensuring consistent environments across development and production.
- Docker Compose: A tool for defining and running multi-container Docker applications, simplifying the setup and deployment process.
- Chi Router: A lightweight, idiomatic HTTP router for Go, providing a flexible and composable way to build APIs.
- API Application: The main service handling HTTP requests for currency conversion, user management, and currency administration.
- PostgreSQL Database: Stores user data, currency information, and logs.
- Redis Cache: Caches frequently accessed exchange rates for improved performance.
- Migrator: A standalone service for running database migrations to create and update the database schema, as well as seeding the database for a default admin user.
- Rate Updater: A standalone service that fetches the latest exchange rates from an external API and updates the database and the cache with the new values.
- Real-time currency conversion
- User registration and authentication
- Admin-only currency management (add, update, remove currencies)
- Rate limiting to prevent abuse
- Logging and auditing of operations
- Scheduled updates of exchange rates
- Caching of frequently accessed data
- Comprehensive error handling and logging
- Containerized deployment for easy scaling and management
This section outlines how to setup and configure this api, including environment variables and important constants.
One thing that is important to note is that the API uses an external service to get the exchange rates, so you will need
an API token to use the service. To facilitate this proccess please use this API Key: 75cc9115d3524769a498914d118e093a
Create a .env
file in the project root with the following variables:
REDIS_PASSWORD
: Password for the Redis instance.REDIS_ADDR
: Address and port of the Redis instance (e.g., "localhost:6379").POSTGRES_USER
: Username for the PostgreSQL database.POSTGRES_PASSWORD
: Password for the PostgreSQL database.POSTGRES_HOST
: Hostname of the PostgreSQL database.POSTGRES_PORT
: Port number for the PostgreSQL database.POSTGRES_NAME
: Name of the PostgreSQL database.API_KEY
: API key for the OpenExchangeRates.SERVER_PORT
: Port on which the API server will listen.
Example .env
file:
REDIS_PASSWORD=myredispassword
REDIS_ADDR=redis:6379
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_HOST=db
POSTGRES_PORT=5432
POSTGRES_NAME=currency_db
API_KEY=75cc9115d3524769a498914d118e093a
SERVER_PORT=8080
To modify and add more environment variables, please create and validate them in the internal/commons/config.go
file.
The internal/commons/constants.go
file contains important constants that can be adjusted to fine-tune the API's behavior:
AllowedCurrencyLength
: Maximum length of currency codes (default: 5). For this one you'll also need to change the005_expand_currency_code.sql
migrationMinimumCurrencyLength
: Minimum length of currency codes (default: 3).AllowedRPS
: Rate limit for API requests per second (default: 10).ExternalClientMaxRetries
: Maximum number of retries for external API calls (default: 3).ExternalClientBaseDelay
: Base delay for exponential backoff in external API calls (default: 1 second).ExternalClientMaxDelay
: Maximum delay for exponential backoff in external API calls (default: 30 seconds).RateUpdaterCacheExipiration
: Expiration time for cached exchange rates (default: 1 hour).RateUpdaterInterval
: Interval for updating exchange rates (default: 1 hour).WorkerHeartbeatInterval
: Interval for worker heartbeat (default: 5 minutes).ServerIdleTimeout
: Server idle timeout (default: 1 minute).ServerReadTimeout
: Server read timeout (default: 10 seconds).ServerWriteTimeout
: Server write timeout (default: 30 seconds).
To modify these constants, edit the internal/commons/constants.go
file and rebuild the application.
To run the application using Docker, follow these steps:
-
Ensure you have Docker and Docker Compose installed on your system.
-
Clone the repository:
git clone [email protected]:Lutefd/currency-api.git cd currency-api
-
Create a
.env
file in the project root and configure the necessary environment variables, a.env.sample
file is provided (see API Configuration docs for details). -
Build and start the Docker containers:
docker compose up --build
-
The API will be available at
http://localhost:8080
if you're using the default port and host provided in the.env.sample
. -
For Endpoints like currency creation, update and delete you will need to use the admin user's API Key created by the migration, the credentials are:
{
"username": "admin",
"password": "password"
}
For local development without Docker:
- Ensure you have Go 1.22 or later installed.
- Install goose for database migrations.
- Set up the environment variables as described in the API Configuration docs.
- Run the database migrations:
make migrate-up
- Start the application:
make run
- For Endpoints like currency creation, update and delete you will need to use the admin user created by the migration, the credentials are:
{ "username": "admin", "password": "password" }
Run the test suite with:
make test
For a more verbose output:
go test ./... -v
There is a Postman collection in the postman-collection.json
file inside of the docs/postman
folder. You can import this collection into Postman and test the API endpoints.
This section details the endpoints and operations available in this API. More information can be found in the Swagger documentation, in the c4 diagram, in the dependencies diagram, and in the entity diagram all located in the docs
folder along with the Postman collection.
All endpoints are relative to: http://localhost:8080/api/v1
The API documentation is available at http://localhost:8080/api/v1/reference
Most endpoints require authentication using an API key. Include the API key in the X-API-Key
header of your requests.
Get the API Key by logging in the `/api/v1/auth/login in with the admin user created by the migration, the credentials are:
{
"username": "admin",
"password": "password"
}
Convert an amount from one currency to another.
Query Parameters:
from
: Source currency code (e.g., "USD")to
: Target currency code (e.g., "EUR")amount
: Amount to convert (numeric)
Example Request:
GET /api/v1/currency/convert?from=USD&to=EUR&amount=100
Example Response:
{
"from": "USD",
"to": "EUR",
"amount": 100,
"result": 85
}
Add a new currency.
Request Body:
{
"code": "JPY",
"rate_to_usd": 110.5
}
Example Response:
{
"message": "currency added successfully"
}
Update an existing currency.
Request Body:
{
"rate_to_usd": 111.2
}
Example Response:
{
"message": "currency updated successfully"
}
Remove a currency.
Example Response:
{
"message": "currency removed successfully"
}
Register a new user.
Request Body:
{
"username": "newuser",
"password": "securepassword"
}
Example Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "newuser",
"role": "user",
"api_key": "your-api-key-here"
}
Authenticate a user and retrieve their API key.
Request Body:
{
"username": "existinguser",
"password": "userpassword"
}
Example Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"username": "existinguser",
"role": "user",
"api_key": "your-api-key-here"
}
Update(ctx context.Context, username, password string) error
The API uses standard HTTP status codes to indicate the success or failure of requests. In case of an error, the response body will contain an error message:
{
"error": "Description of the error"
}
Common error status codes:
- 400: Bad Request (invalid input)
- 401: Unauthorized (missing or invalid API key)
- 403: Forbidden (insufficient permissions)
- 404: Not Found (resource not found)
- 429: Too Many Requests (rate limit exceeded)
- 500: Internal Server Error
The API implements rate limiting to prevent abuse in some endpoints. If you exceed the rate limit, you'll receive a 429 status code. Wait before making additional requests.
- This project used goose to execute migrations, although the migrations were written by hand, the migration code and execute is generated by goose.
- It also uses mermaid to generate the diagrams in the docs folder. The mermaid code is written by hand and the diagrams are generated by the mermaid live editor.
- The swagger UI and its playground are generated by Scalar, the swagger documentation is written by hand.
- The Makefile was extracted from a boilerplate cli tool I've used in a lot of personal projects, but didn't use it in this project, only the makefile was copied from it.
There are a lot of improvements that could be made to this project, I decided to keep it as simple as possible, even removing some features that were not necessary for the base requirements of the project, but some improvements that could be made are:
- Rate limiting using a more robust solution like Redis, this would allow for a more distributed rate limiting system. The current rate limiting is done in memory and is not distributed, using the Rate package, it uses a token bucket algorithm to limit the number of requests per second.
- Logging, the current logging system is very simple, it registers the internal erros and info logs in a postgres database, but it could be improved to use a more robust logging system like the ELK stack. I decided to keep it simple because the requirements didn't ask for a more robust logging system and as I was already using postgres for the database, I decided to use it for the logs as well since it can handle the load, I limited it to about 1000 logs in the channel, so it won't overload the database.
- The rate updater could be improved by making it more robust and based on the last fetched timestamp. Although it has a retry policy and a backoff policy, it could have a circuit breaker to prevent the service from being overloaded if the external service is down for a long time. It could also have a health check mechanism to help its docker service have a health condition. A catch-up mechanism could be added to fetch the rate updates that were missed while the service was down.
- The currency management could be improved by adding more features like a list of all currencies, and a more detailed view of each currency, with the possibility of adding more information like the name of the currency, the symbol, and the country of origin. Also due to currencies that have a lot of decimal places, it could be improved to handle more decimal places while rounding up for the ones that don't need it, currently we're using all of the decimal places provided by the external service.
- The user management and security could be greatly improved, right now it's really simple with users not being able to do much, not even edit their own information, it could be improved by adding more features like password recovery, email verification, and more detailed user information. The security could be improved by adding more security features like 2FA, also adding more than just an X-API-Key for authentication, like JWT tokens. But this was a decision that I made to keep it simple and to focus on the core functionality of the project, while having the minimum user management required to use the API.
- Tests could be better and could cover more of the errors that could happen in the system, I've only covered the basic errors that could happen in the system, but there are a lot of edge cases that could be tested. Also, the tests could be more robust and could use a more robust testing framework like Ginkgo.
I learned a lot while doing this challenge, there were a lot of things that could be improved and I'm going to keep studying so I can improve my skills and be able to do better in my next projects. It was a bit difficult to find the time to do this project, but I'm happy to have done it and I'm happy with the result.