@ordius/adonisjs-currencyx
v2.2.1
Published
AdonisJS integration for CurrencyX with database exchange provider and cache support
Readme
AdonisJS CurrencyX
AdonisJS integration for CurrencyX with database provider and cache support. Seamlessly integrate currency conversion into your AdonisJS applications.
✨ Features
- 🚀 AdonisJS Integration - Seamless integration with AdonisJS v7 framework
- 💾 Database Exchange - Store exchange rates in your database using Lucid ORM
- 🔄 Multiple Exchanges - Google Finance, Fixer.io, and database exchanges
- 📦 Cache Support - Built-in caching with AdonisJS Cache
- 🎯 Type Safety - Full TypeScript support with intelligent inference
- 🔧 Easy Setup - Simple configuration and installation
- 🌐 Service Container - Registered as AdonisJS service for dependency injection
- 🏗️ Repository Pattern - Clean architecture following AdonisJS patterns
📦 Installation
npm install @ordius/adonisjs-currencyx🚀 Setup
1. Configure the package
node ace configure @ordius/adonisjs-currencyxThis will:
- Create
config/currency.tsconfiguration file - Create Currency model and migration stubs
- Register the service provider
2. Configure providers
Edit config/currency.ts:
import env from '#start/env'
import { defineConfig, exchanges } from '@ordius/adonisjs-currencyx'
export default defineConfig({
/*
|--------------------------------------------------------------------------
| Default Exchange
|--------------------------------------------------------------------------
*/
default: env.get('CURRENCY_PROVIDER', 'database') as 'database' | 'google' | 'fixer',
/*
|--------------------------------------------------------------------------
| Exchange Configurations
|--------------------------------------------------------------------------
*/
exchanges: {
/*
|--------------------------------------------------------------------------
| Database Exchange
|--------------------------------------------------------------------------
| Uses your local database to store and retrieve exchange rates.
*/
database: exchanges.database({
model: () => import('#models/currency'),
base: 'USD',
columns: {
code: 'code',
rate: 'exchange_rate',
},
// Cache configuration (optional)
// cache: {
// service: () => import('@adonisjs/cache/services/main'),
// ttl: '1h', // Cache TTL (human readable or milliseconds)
// prefix: 'currency' // Cache namespace for this exchange's entries
// }
}),
/*
|--------------------------------------------------------------------------
| Google Finance Exchange
|--------------------------------------------------------------------------
| Free exchange using Google Finance API. No API key required.
*/
google: exchanges.google({
base: env.get('CURRENCY_BASE', 'USD'),
timeout: 5000,
}),
/*
|--------------------------------------------------------------------------
| Fixer.io Exchange
|--------------------------------------------------------------------------
| Requires API key from fixer.io.
*/
// fixer: exchanges.fixer({
// accessKey: env.get('FIXER_API_KEY'),
// base: env.get('CURRENCY_BASE', 'USD'),
// timeout: 10000
// })
},
})3. Create Currency model
node ace make:model Currency// app/models/currency.ts
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'
export default class Currency extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare code: string
@column()
declare name: string
@column()
declare exchange_rate: number
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}4. Create migration
node ace make:migration create_currencies_table// database/migrations/xxx_create_currencies_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'currencies'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('code', 3).notNullable().unique()
table.string('name').notNullable()
// Scale 15: crypto rates (1 USD ≈ 0.0000130 BTC) need it — a narrow scale rounds them away
table.decimal('exchange_rate', 30, 15).notNullable()
table.timestamp('created_at')
table.timestamp('updated_at')
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}5. Run migration
node ace migration:run💡 Usage
Basic Usage in Controllers
// app/controllers/exchange_controller.ts
import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import CurrencyService from '@ordius/adonisjs-currencyx/services/main'
@inject()
export default class ExchangeController {
constructor(private currency: CurrencyService) {}
async convert({ request, response }: HttpContext) {
const { amount, from, to } = request.only(['amount', 'from', 'to'])
const result = await this.currency.convert({
amount: Number(amount),
from,
to,
})
if (result.success) {
return response.json({
success: true,
data: {
amount,
from,
to,
result: result.result,
rate: result.info.rate,
timestamp: result.info.timestamp,
},
})
}
return response.status(400).json({
success: false,
error: result.error?.info,
})
}
async getRates({ request, response }: HttpContext) {
const { base, codes } = request.only(['base', 'codes'])
const result = await this.currency.getExchangeRates({
base,
codes: codes.split(','),
})
if (result.success) {
return response.json({
success: true,
data: result,
})
}
return response.status(400).json({
success: false,
error: result.error?.info,
})
}
}Exchange Switching
// Switch to different exchange at runtime
await this.currency.use('google')
const googleResult = await this.currency.convert({ amount: 100, from: 'USD', to: 'EUR' })
await this.currency.use('fixer')
const fixerResult = await this.currency.convert({ amount: 100, from: 'USD', to: 'EUR' })
await this.currency.use('database')
const dbResult = await this.currency.convert({ amount: 100, from: 'USD', to: 'EUR' })Database Exchange Usage
Seed your database with exchange rates:
// database/seeders/currency_seeder.ts
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import Currency from '#models/currency'
export default class extends BaseSeeder {
async run() {
const rates = [
{ code: 'USD', name: 'US Dollar', exchange_rate: 1.0 },
{ code: 'EUR', name: 'Euro', exchange_rate: 0.85 },
{ code: 'GBP', name: 'British Pound', exchange_rate: 0.73 },
{ code: 'JPY', name: 'Japanese Yen', exchange_rate: 110.0 },
{ code: 'CAD', name: 'Canadian Dollar', exchange_rate: 1.25 },
{ code: 'AUD', name: 'Australian Dollar', exchange_rate: 1.35 },
]
for (const rate of rates) {
await Currency.updateOrCreate({ code: rate.code }, rate)
}
}
}Run the seeder:
node ace db:seedCaching
Enable caching for better performance:
// config/currency.ts
database: exchanges.database({
model: () => import('#models/currency'),
base: 'USD',
columns: {
code: 'code',
rate: 'exchange_rate',
},
cache: {
service: () => import('@adonisjs/cache/services/main'), // AdonisJS cache service
ttl: '1h', // Cache TTL (human readable or milliseconds)
prefix: 'currency', // Cache namespace for this exchange's entries
},
})Everything the exchange caches — the rate list and one entry per currency pair convert() /
getConvertRate() looked up — lives under the prefix namespace. Clear it whenever you write
rates, or readers keep serving the previous ones for the full ttl:
// e.g. at the end of a command/job that syncs rates into the table
await currency.use('database').clearCache()clearCache() works from any process, including one that has not read rates yet.
Base currency
Rows are quoted against the exchange's base (1 base buys exchange_rate units of the row's
currency), so write them in that base — rebase before storing if your provider answers in another
one. Asking for a different base derives it from the stored rates:
await currency.use('database').latestRates({ base: 'EUR' }) // every rate re-quoted per 1 EURA base with no row in the table returns success: false with UNSUPPORTED_CURRENCY. decimal
columns that the driver returns as strings are read as numbers.
📚 API Reference
The AdonisJS integration provides the same API as the core CurrencyX package:
Core Methods
// Convert currency
const result = await currency.convert({
amount: 100,
from: 'USD',
to: 'EUR',
})
// Get exchange rates
const rates = await currency.getExchangeRates({
base: 'USD',
codes: ['EUR', 'GBP', 'JPY'],
})Convenience Methods
// Shorthand for getting rates
const rates = await currency.latestRates({ base: 'USD', codes: ['EUR', 'GBP'] })
// Get exchange rates with same API as core library
const rates = await currency.getExchangeRates({ base: 'USD', codes: ['EUR', 'GBP'] })Exchange Management
// Switch exchanges
currency.use('google')
// Read one without switching the active exchange
const google = currency.get('google')
// Get current exchange
const current = currency.getCurrentExchange()
// List available exchanges
const exchanges = currency.getAvailableExchanges()
// Narrow user input (a CLI flag, a query param) to a configured exchange
if (currency.has(name)) {
currency.use(name)
}
get()/has()need@mixxtor/currencyx-js>= 2.4.0.
Utility Methods
// Format currency (object parameters)
const formatted = currency.formatCurrency({ amount: 1234.56, code: 'USD', locale: 'en-US' })
// Round values
const rounded = currency.round(123.456789, { precision: 2, direction: 'up' })
// Get supported currencies
const currencies = await currency.getSupportedCurrencies()
// Currency information utilities
const allCurrencies = currency.getList()
const usdInfo = currency.getByCode('USD')
const dollarCurrencies = currency.getBySymbol('$')
const usCurrency = currency.getByCountry('US')
const euroCurrencies = currency.filterByName('Euro')
const usCurrencies = currency.filterByCountry('US')
// Round money according to currency rules
const rounded = currency.roundMoney(123.456, 'USD')⚙️ Configuration
Environment Variables
Add these to your .env file:
# Default exchange
CURRENCY_PROVIDER=database
# Base currency
CURRENCY_BASE=USD
# Fixer.io API key (if using fixer exchange)
FIXER_API_KEY=your_api_key_hereExchange Configuration
Database Exchange
database: exchanges.database({
model: () => import('#models/currency'), // Your Currency model
base: 'USD', // Base currency
columns: {
code: 'code', // Currency code column
rate: 'exchange_rate', // Exchange rate column
},
cache: {
// Optional caching
service: () => import('@adonisjs/cache/services/main'), // AdonisJS cache service
ttl: '1h', // Cache TTL (human readable or milliseconds)
prefix: 'currency', // Cache namespace for this exchange's entries
},
})Google Finance Exchange
google: exchanges.google({
base: 'USD', // Base currency
timeout: 5000, // Request timeout (optional)
})Fixer.io Exchange
fixer: exchanges.fixer({
accessKey: 'your-api-key', // Required
base: 'USD', // Base currency (default: 'USD' for this library)
timeout: 10000, // Request timeout (optional)
})Your own exchange
An exchange this package does not ship — a private rate service, a provider behind your own API
key, anything a public package could not carry — takes two steps, and neither involves a registry:
write it with createExchange(), register it with defineExchange().
// app/services/currency_exchanges/mx_exchange_service.ts
import { createExchange, CurrencyError, ConfigurationError } from '@ordius/adonisjs-currencyx'
import type { CurrencyCode } from '@ordius/adonisjs-currencyx'
export type MxConfig = { accessKey: string; base?: CurrencyCode; timeout?: number }
export class MxExchange extends createExchange<MxConfig>({
name: 'mx',
defaults: { base: 'EUR', timeout: 5000 },
// What the upstream really does, so the generated class compensates:
// base → the ONLY base it publishes; any other base is derived locally
// supportsCodes → false means it ignores `symbols`, so filtering happens here
upstream: { base: 'EUR', supportsCodes: false },
validate: (config) => {
if (!config.accessKey) throw new ConfigurationError('Mx exchange requires an accessKey')
},
setKey: (config, key) => (config.accessKey = key),
async fetchRates({ config, signal }) {
const url = new URL('https://currencyrates.example.dev')
url.searchParams.set('access_key', config.accessKey)
const response = await fetch(url, { signal }) // `signal` already honours config.timeout
const data = await response.json()
if (!response.ok || !data.success) {
throw new CurrencyError(
data.error ?? `HTTP ${response.status}`,
response.status,
'INVALID_ACCESS_KEY'
)
}
return data.rates
},
}) {}That is the whole exchange: latestRates, convert, getConvertRate, rebasing, filtering and
error results come from createExchange. Pair-based APIs (one quote per request) declare
fetchRate instead — see the @mixxtor/currencyx-js docs
for the full spec, and subclass BaseCurrencyExchange directly when an API cannot be described
this way. Both are re-exported here, so an exchange packaged separately needs only this package as
a peer dependency.
// config/currency.ts
import env from '#start/env'
import { defineConfig, defineExchange, exchanges } from '@ordius/adonisjs-currencyx'
import { MxExchange } from '#services/currency_exchanges/mx_exchange_service'
const currencyConfig = defineConfig({
default: env.get('CURRENCY_EXCHANGE_PROVIDER', 'database'),
exchanges: {
database: exchanges.database({ model: () => import('#models/currency') }),
mx: defineExchange(() => new MxExchange({ accessKey: env.get('MX_CURRENCY_API_KEY') })),
},
})
export default currencyConfig
declare module '@ordius/adonisjs-currencyx/types' {
interface CurrencyExchanges extends InferExchanges<typeof currencyConfig> {}
}currency.use('mx') is now typed and InferExchanges reports MxExchange — the same treatment
the bundled exchanges get. The config is the registration.
The resolver receives (name, app) and may be async, so an exchange can be built from the
container instead of at module-import time:
mx: defineExchange(async (name, app) => {
const logger = await app.container.make('logger')
return new MxExchange({ accessKey: env.get('MX_CURRENCY_API_KEY'), logger: logger.child({ exchange: name }) })
}),A plain instance (mx: new MxExchange({ ... })) still works and stays the shortest form for an
exchange that needs nothing from the app. defineExchange() buys laziness and container access.
🛡️ Error Handling
All methods return result objects with success indicators:
const result = await currency.convert({
amount: 100,
from: 'USD',
to: 'EUR',
})
if (result.success) {
// Handle success
console.log(`Converted: ${result.result}`)
console.log(`Rate: ${result.info.rate}`)
console.log(`Timestamp: ${result.info.timestamp}`)
} else {
// Handle error
console.error(`Error: ${result.error?.info}`)
console.error(`Type: ${result.error?.type}`)
}🧪 Testing
The package includes comprehensive tests. Run them with:
npm testFor development testing:
npm run quick:testFor test coverage:
npm run test
# Coverage report will be generated in ./coverage/📋 Requirements
- Node.js >= 20.6.0
- AdonisJS >= 6.19.0
- @adonisjs/lucid >= 21.7.0 (for database provider)
- @adonisjs/cache >= 1.3.0 (optional, for caching)
🤝 Contributing
Contributions are welcome! Please read the contributing guidelines before submitting PRs.
📄 License
MIT License - see LICENSE.md file for details.
📦 Related Packages
- @mixxtor/currencyx-js - Core currency conversion library
Documentation • Issues • Contributing
Made with ❤️ by Mixxtor
