Skip to content

Commit

Permalink
Merge pull request #16 from TextureHQ/victor/enable-provider-fallback
Browse files Browse the repository at this point in the history
Now supporting multiple providers with automatic fallback
  • Loading branch information
victorquinn authored Oct 9, 2024
2 parents 3c4093e + a7b297e commit 386922d
Show file tree
Hide file tree
Showing 7 changed files with 552 additions and 172 deletions.
275 changes: 204 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,150 +4,283 @@

An awesome TypeScript weather client for fetching weather data from various weather providers.

## What makes it "plus"?
## What Makes It "Plus"?

* Support for various weather providers:
* [National Weather Service](https://weather-gov.github.io/api/)
* [OpenWeather](https://openweathermap.org/api)
* [Tomorrow](https://www.tomorrow.io/) (coming soon!)
* [WeatherKit](https://developer.apple.com/weatherkit/) (coming soon!)
* A clean and standardized API for fetching weather data regardless of the weather provider
* Baked in support for caching with [Redis](https://redis.io/)
* Built with TypeScript so it includes a type-safe API out of the box
- **Multiple Weather Providers with Fallback Support**: Seamlessly switch between weather providers and specify an ordered list for fallback.
- [National Weather Service](https://weather-gov.github.io/api/)
- [OpenWeather](https://openweathermap.org/api)
- [Tomorrow.io](https://www.tomorrow.io/) (coming soon!)
- [WeatherKit](https://developer.apple.com/weatherkit/) (coming soon!)
- **Clean and Standardized API**: Fetch weather data using a consistent interface, regardless of the underlying provider.
- **Built-in Caching**: Supports in-memory and Redis caching to optimize API usage and reduce latency.
- **TypeScript Support**: Enjoy a type-safe API out of the box for better development experience.

## Installation

Install the package using npm or yarn:

```bash
npm install weather-plus

or

yarn add weather-plus
```

## Usage
First import the library into your project:

First, import the library into your project:

import WeatherPlus from 'weather-plus';

or with CommonJS:

const WeatherPlus = require('weather-plus').default;

### Basic Usage

Instantiate the WeatherPlus class with default options:

```ts
import { WeatherPlus } from 'weather-plus';
const weatherPlus = new WeatherPlus();
```

or with CommonJS:
Fetch weather data:

```ts
const { WeatherPlus } = require('weather-plus');
const weather = await weatherPlus.getWeather(40.748817, -73.985428); // Coordinates for New York City
console.log(weather);
```

and then instantiate it
### Using Multiple Providers with Fallback

You can specify an ordered list of providers for fallback. If the first provider fails (e.g., due to unsupported location), the next provider in the list will be tried.

```ts
const weatherPlus = new WeatherPlus();
const weatherPlus = new WeatherPlus({
providers: ['nws', 'openweather'],
apiKeys: {
openweather: 'your-openweather-api-key', // Replace with your actual API key
},
});
```

and then use it
### Example with Providers and API Keys

```ts
const weather = await weatherPlus.getWeather(40.748020, -73.992400)
console.log(weather)
const weatherPlus = new WeatherPlus({
providers: ['nws', 'openweather'],
apiKeys: {
openweather: 'your-openweather-api-key',
},
cacheTTL: 600, // Optional: Cache TTL in seconds (default is 300 seconds)
});
```

## Providers
Fetch weather data:

```ts
const weather = await weatherPlus.getWeather(51.5074, -0.1278); // Coordinates for London
console.log(weather);
```

Part of the main benefit of this library is the ability to seamlessly switch between weather providers while maintaining a consistent API. You can use this to hit NWS for free and fallback to a paid provider if the user is in an area with limited NWS coverage.
### Providers

If you provide no provider, it will default to using the National Weather Service.
One of the main benefits of this library is the ability to seamlessly switch between weather providers while maintaining a consistent API. This is particularly useful for:

NWS is used by default because it's free and doesn't require an API key. However, a few important caveats
* It only supports locations in the United States
* It is rate limited so if you make too many requests (more than 5 per second or 300 per minute) it will rate limit you
• Fallback Mechanism: Use a free provider by default and fallback to a paid provider if necessary.
• Coverage: Some providers may not support certain locations; having multiple providers ensures broader coverage.
• Cost Optimization: Reduce costs by prioritizing free or cheaper providers.

To use a different provider, you can pass in a string key for the provider to the constructor. For example, to use OpenWeather:
Available Providers

- 'nws' - National Weather Service
- Notes:
- Free and doesn’t require an API key.
- Only supports locations within the United States.
- Rate-limited to 5 requests per second and 300 requests per minute.
- 'openweather' - OpenWeather
- Requires an API key.
- 'tomorrow.io' - Tomorrow.io (coming soon!)
- 'weatherkit' - Apple WeatherKit (coming soon!)

### Specifying Providers

You can specify the providers in order of preference:

```ts
const weatherPlus = new WeatherPlus({
provider: "openweather",
apiKey: "your-openweather-api-key",
providers: ['nws', 'openweather'],
apiKeys: {
openweather: 'your-openweather-api-key',
},
});
```

The following providers are available:
* `nws` - National Weather Service
* `openweather` - OpenWeather
* `tomorrow` - Tomorrow.io (coming soon!)
* `weatherkit` - Apple WeatherKit (coming soon!)
### API Keys

## Built-in caching
Some providers require API keys. Provide them using the apiKeys object, mapping provider names to their respective API keys.

There are multiple ways to use caching with this library.
```ts
const weatherPlus = new WeatherPlus({
providers: ['openweather'],
apiKeys: {
openweather: 'your-openweather-api-key',
},
});
```

Out of the box, if no redis client is provided, it will use an in-memory cache.
### Built-in Caching

To optimize API usage and reduce latency, weather-plus includes built-in caching mechanisms.

#### In-Memory Cache

By default, if no Redis client is provided, the library uses an in-memory cache.

```ts
// No redis client, uses in-memory cache
const weatherPlus = new WeatherPlus();
```

This cache is not persisted across sessions and is not recommended for production use and because it is in-memory, it will not help across containers or nodes since the cache is scoped to the instance of the application. However, it will help reduce the volume of API requests which helps with cost and rate limiting.
Note: The in-memory cache is not shared across different instances or servers. It’s suitable for development and testing but not recommended for production environments where you have multiple server instances.

#### Redis Cache

If you want to use a shared Redis cache across instances of your application, you can pass in a redis client instance which `weather-plus` will use to store and retrieve weather data.
For production use, it’s recommended to use a Redis cache, which allows sharing cached data across different instances of your application.

```ts
// With a redis client
const redisClient = redis.createClient();
import { createClient } from 'redis';

const redisClient = createClient();
await redisClient.connect();

const weatherPlus = new WeatherPlus({
redisClient,
redisClient,
});
```

## Geohash
### Cache Time-to-Live (TTL)

There is another layer of caching that is built in to the library. When you supply a raw lat/lng, we convert that to a geohash and use that as the key for the cache. This means that weather data for the same location (or a decent area around the point) will be cached across multiple requests.
You can customize the cache TTL (time-to-live) in seconds. The default TTL is 300 seconds (5 minutes).

If you are unfamiliar with geohashes, they are a way to represent a location using a base-32 string of latitude and longitude coordinates. For example, the geohash for an area around the Empire State Building is `dr5ru6`.
```ts
const weatherPlus = new WeatherPlus({
cacheTTL: 600, // Cache data for 10 minutes
});
```

### Geohash Precision

We use a geohash of 5 characters to represent a location. This means that the area covered by a geohash is roughly a 4.9km x 4.9km rectangle (3 miles x 3 miles) at the equator which means that any 2 points that are anywhere within that rectangle will have the same geohash which means that the weather data for those points will be cached together.
The library uses geohashing to cache weather data for nearby locations efficiently. Geohashing converts latitude and longitude into a short alphanumeric string, representing an area on the Earth’s surface.

Given that weather data doesn't change much on the scale of a few kilometers, this can be a very effective way to reduce the number of API requests you make, especially if you have many requests near the same location.
#### Default Geohash Precision

Geohashes are on always but you can alter the precision. If you would like to functionally "opt out" of them you can provide a precision like `geohashPrecision: 12` in the options. This will generate a geohash with a precision of 12 which means that the area covered by a geohash is roughly a 37.2mm x 18.6mm rectangle (1.46 inches x 0.73 inches) which is so specific as to not be useful for caching weather data across requests for near locations.
By default, the geohash precision is set to 5, which corresponds to an area of approximately 4.9 km x 4.9 km.

```ts
// No geohash precision, defaults to 5
const weatherPlus = new WeatherPlus();
```

#### Customizing Geohash Precision

You can adjust the geohashPrecision to broaden or narrow the caching area.

// Geohash precision of 7
```ts
// Broader caching area (less precise)
const weatherPlus = new WeatherPlus({
geohashPrecision: 7,
geohashPrecision: 3, // Approximately 156 km x 156 km
});

// Geohash precision of 12, effectively "opting out" of geohashing by choosing a precision that is so small it doesn't help with caching
// Narrower caching area (more precise)
const weatherPlus = new WeatherPlus({
geohashPrecision: 12,
geohashPrecision: 7, // Approximately 610 m x 610 m
});
```

Note if you "opt out" of geohashing by providing a precision of 12 or more, the library will not be able to cache weather data across requests for near locations so your API request rate limits will be higher.
Note: A lower precision value results in a larger area being considered the same location for caching purposes. Adjust according to your application’s requirements.

You can also opt to broaden caching by providing a `geohashPrecision` of less than 5. This will cause the library to cache weather data for a larger area which can help reduce the number of API requests but will cause less specific caching. For example, a `geohashPrecision` of 3 will cache weather data for an area roughly 156km x 156km (97 miles x 97 miles) which is a decent sized area but probably good enough for caching temperature within some degree of accuracy in some use cases.
### Error Handling

We have this option for those who want to broaden or narrow the caching area.
The library provides custom error classes to help you handle specific error scenarios gracefully.

## Cache TTL
#### InvalidProviderLocationError

You can also set a custom TTL for the cache. This is the amount of time in seconds that the cache will store the weather data. If no TTL is provided, the cache will store the weather data for 5 minutes.
This error is thrown when a provider does not support the requested location.

```ts
// No cache TTL, uses default of 300 seconds
const weatherPlus = new WeatherPlus();

// Cache TTL of 3600 seconds (1 hour)
const weatherPlus = new WeatherPlus({
redisClient,
cacheTTL: 3600, // 1 hour
});
import { InvalidProviderLocationError } from 'weather-plus';

try {
const weather = await weatherPlus.getWeather(51.5074, -0.1278); // London coordinates
} catch (error) {
if (error instanceof InvalidProviderLocationError) {
// Handle the error (e.g., notify the user or log the issue)
} else {
// Handle other types of errors
}
}
```

Note the ttl works both for the in-memory cache and for the redis cache.
### TypeScript Support

## Types
This library is built with TypeScript and includes type-safe interfaces for weather data and errors.

This library is built with TypeScript and includes type-safe interfaces for the weather data and errors.
#### Weather Data Interface

## License
```ts
interface IWeatherData {
temperature: {
value: number;
unit: string;
};
humidity: {
value: number;
unit: string;
};
dewPoint: {
value: number;
unit: string;
};
conditions: {
value: string;
unit: string;
};
}
```

MIT
### Complete Example

```ts
import WeatherPlus from 'weather-plus';
import { createClient } from 'redis';

(async () => {
const redisClient = createClient();
await redisClient.connect();

const weatherPlus = new WeatherPlus({
providers: ['nws', 'openweather'],
apiKeys: {
openweather: 'your-openweather-api-key',
},
redisClient,
geohashPrecision: 5,
cacheTTL: 600, // 10 minutes
});

try {
const weather = await weatherPlus.getWeather(51.5074, -0.1278); // London
console.log(weather);
} catch (error) {
console.error('Error fetching weather data:', error);
} finally {
await redisClient.disconnect();
}
})();
```

## License

MIT

Lovingly crafted in NYC by [Victor Quinn](https://github.com/victorquinn) at [Texture](https://www.texturehq.com)
Lovingly crafted in NYC by [Victor Quinn](https://github.com/victorquinn) at [Texture](https://www.texturehq.com)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "weather-plus",
"version": "0.1.1",
"version": "1.0.0",
"description": "Weather Plus is a powerful wrapper around various Weather APIs that simplifies adding weather data to your application",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand Down
Loading

0 comments on commit 386922d

Please sign in to comment.