
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@brika/http
Advanced tools
Modern, fully-typed HTTP client for Bun with advanced features including performant caching, interceptors, retry logic, and request deduplication.
@brika/diThis package is part of the Brika workspace. Add it to your dependencies:
{
"dependencies": {
"@brika/http": "workspace:*"
}
}
Then run:
bun install
import { HttpClient } from '@brika/http';
const client = new HttpClient();
// Fetch and get typed response
const response = await client
.get<{ userId: number; id: number; title: string }>('https://jsonplaceholder.typicode.com/todos/1')
.send();
console.log(response.data.title);
import { HttpClient } from '@brika/http';
const client = new HttpClient();
// First call - hits the API
const response1 = await client
.get('https://api.example.com/users')
.cache({ ttl: 60_000 }) // Cache for 1 minute
.send();
console.log(response1.cached); // false
// Second call - returns cached result
const response2 = await client
.get('https://api.example.com/users')
.cache({ ttl: 60_000 })
.send();
console.log(response2.cached); // true
import { HttpClient } from '@brika/http';
const client = new HttpClient();
// GET request
const users = await client.get<User[]>('/users').send();
// POST request with JSON body
const newUser = await client
.post<User>('/users')
.json({ name: 'John Doe', email: 'john@example.com' })
.send();
// PUT request
await client
.put('/users/123')
.json({ name: 'Jane Doe' })
.send();
// PATCH request
await client
.patch('/users/123')
.json({ email: 'new@example.com' })
.send();
// DELETE request
await client.delete('/users/123').send();
// Get only the data (not full response)
const userData = await client.get<User>('/users/123').data();
HttpClient uses the @singleton() decorator, making it easy to inject into your services:
import { HttpClient } from '@brika/http';
import { singleton, inject } from '@brika/di';
@singleton()
class UserService {
readonly #http = inject(HttpClient);
async getUser(id: string): Promise<User> {
return this.#http
.get<User>(`/users/${id}`)
.cache({ ttl: 300_000 }) // Cache for 5 minutes
.data();
}
async createUser(name: string, email: string): Promise<User> {
return this.#http
.post<User>('/users')
.json({ name, email })
.data();
}
async searchUsers(query: string): Promise<User[]> {
return this.#http
.get<User[]>('/users/search')
.params({ q: query, limit: '20' })
.cache({ ttl: 60_000, tags: ['users', 'search'] })
.data();
}
}
Configure the client with defaults:
import { HttpClient, MemoryCache } from '@brika/http';
const client = new HttpClient({
// Base URL for all requests
baseUrl: 'https://api.example.com',
// Default headers
headers: {
'User-Agent': 'MyApp/1.0',
'Accept': 'application/json',
},
// Request timeout (default: 30000ms)
timeout: 30_000,
// Enable caching (default: enabled with MemoryCache)
cache: new MemoryCache(),
// Retry configuration
retry: {
maxAttempts: 3,
backoff: 'exponential',
delay: 1000,
maxDelay: 30_000,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
},
// Custom interceptors
interceptors: {
request: [(config) => {
// Add auth token to all requests
return {
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${getToken()}`,
},
};
}],
},
});
| Method | Description |
|---|---|
get<T>(url) | Make a GET request |
post<T>(url, body?) | Make a POST request |
put<T>(url, body?) | Make a PUT request |
patch<T>(url, body?) | Make a PATCH request |
delete<T>(url) | Make a DELETE request |
head<T>(url) | Make a HEAD request |
options<T>(url) | Make an OPTIONS request |
execute<T>(config) | Execute with full RequestConfig |
clearCache() | Clear all cached entries |
invalidateCache(tag) | Invalidate cache by tag |
invalidateCacheTags(tags[]) | Invalidate cache by multiple tags |
All HTTP methods return a RequestBuilder with these chainable methods:
| Method | Description | Example |
|---|---|---|
.params(params) | Set query parameters | .params({ page: '1', limit: '10' }) |
.headers(headers) | Set multiple headers | .headers({ 'Accept': 'application/json' }) |
.header(key, value) | Set single header | .header('Authorization', 'Bearer token') |
.body(body) | Set request body | .body(formData) |
.json(data) | Set JSON body | .json({ name: 'John' }) |
.timeout(ms) | Set request timeout | .timeout(5000) |
.cache(options) | Set cache options | .cache({ ttl: 60000 }) |
.retry(config) | Set retry config | .retry({ maxAttempts: 3 }) |
.signal(signal) | Set AbortSignal | .signal(controller.signal) |
.send() | Execute and return full response | await .send() |
.data() | Execute and return only data | await .data() |
interface HttpClientConfig {
baseUrl?: string; // Base URL for all requests
headers?: HttpHeaders; // Default headers
timeout?: number; // Default timeout (ms)
cache?: CacheAdapter; // Cache adapter instance
retry?: RetryConfig; // Retry configuration
interceptors?: { // Custom interceptors
request?: RequestInterceptor[];
response?: ResponseInterceptor[];
error?: ErrorInterceptor[];
};
}
interface CacheOptions {
ttl: number; // Time to live (ms)
key?: string; // Custom cache key
tags?: string[]; // Tags for group invalidation
skip?: boolean; // Skip cache for this request
refresh?: boolean; // Force refresh (ignore cache)
}
interface RetryConfig {
maxAttempts: number; // Max retry attempts
backoff: 'linear' | 'exponential'; // Backoff strategy
delay: number; // Initial delay (ms)
maxDelay?: number; // Maximum delay (ms)
retryableStatusCodes?: number[]; // Status codes to retry
shouldRetry?: (error, attempt) => boolean; // Custom predicate
}
// Cache for 1 minute
await client.get('/users')
.cache({ ttl: 60_000 })
.send();
// Cache with custom key
await client.get('/users')
.cache({ ttl: 60_000, key: 'all-users' })
.send();
// Skip cache for this request
await client.get('/users')
.cache({ ttl: 60_000, skip: true })
.send();
// Force refresh (bypass cache)
await client.get('/users')
.cache({ ttl: 60_000, refresh: true })
.send();
// Cache with tags
await client.get('/users')
.cache({ ttl: 60_000, tags: ['users', 'list'] })
.send();
await client.get('/users/123')
.cache({ ttl: 60_000, tags: ['users', 'user-123'] })
.send();
// Invalidate all 'users' tagged cache
client.invalidateCache('users');
// Invalidate multiple tags
client.invalidateCacheTags(['users', 'posts']);
Implement the CacheAdapter interface:
import type { CacheAdapter } from '@brika/http';
class RedisCache implements CacheAdapter {
#redis: Redis;
constructor(redis: Redis) {
this.#redis = redis;
}
async get<T>(key: string): Promise<T | null> {
const value = await this.#redis.get(key);
return value ? JSON.parse(value) : null;
}
async set<T>(key: string, value: T, ttl: number, tags?: string[]): Promise<void> {
await this.#redis.setex(key, Math.floor(ttl / 1000), JSON.stringify(value));
// Store tag associations
if (tags) {
for (const tag of tags) {
await this.#redis.sadd(`tag:${tag}`, key);
}
}
}
async delete(key: string): Promise<void> {
await this.#redis.del(key);
}
async has(key: string): Promise<boolean> {
return (await this.#redis.exists(key)) > 0;
}
async clear(): Promise<void> {
await this.#redis.flushdb();
}
async invalidateByTag(tag: string): Promise<void> {
const keys = await this.#redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
await this.#redis.del(...keys);
}
await this.#redis.del(`tag:${tag}`);
}
async invalidateByTags(tags: string[]): Promise<void> {
for (const tag of tags) {
await this.invalidateByTag(tag);
}
}
}
// Use custom cache
const client = new HttpClient({
cache: new RedisCache(redis),
});
Automatically retries failed requests with configurable backoff:
import { HttpClient } from '@brika/http';
const client = new HttpClient({
retry: {
maxAttempts: 3,
backoff: 'exponential', // or 'linear'
delay: 1000, // Initial delay: 1s
maxDelay: 30_000, // Max delay: 30s
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
},
});
Automatically added when timeout is configured:
const client = new HttpClient({
timeout: 30_000, // 30 seconds
});
Log all requests, responses, and errors:
import { HttpClient, LoggerInterceptor } from '@brika/http';
const logger = new LoggerInterceptor({
logRequests: true,
logResponses: true,
logErrors: true,
logger: console, // Custom logger
});
const client = new HttpClient({
interceptors: {
request: [logger],
response: [logger],
error: [logger],
},
});
Automatically prevents duplicate in-flight GET requests (enabled by default).
import { HttpClient } from '@brika/http';
const client = new HttpClient({
interceptors: {
// Request interceptor - modify request before sending
request: [
(config) => {
// Add timestamp to all requests
return {
...config,
params: {
...config.params,
_t: Date.now(),
},
};
},
],
// Response interceptor - transform response data
response: [
(response) => {
// Unwrap data from API envelope
return {
...response,
data: response.data.result,
};
},
],
// Error interceptor - handle or recover from errors
error: [
async (error, config) => {
if (error.status === 401) {
// Refresh token and retry
await refreshToken();
return client.execute(config);
}
throw error;
},
],
},
});
import { HttpError, TimeoutError, isHttpError, isTimeoutError } from '@brika/http';
try {
await client.get('/users/123').send();
} catch (error) {
if (isTimeoutError(error)) {
console.error('Request timed out:', error.timeout);
} else if (isHttpError(error)) {
console.error('HTTP error:', error.status, error.message);
// Check error type
if (error.isClientError) {
console.log('Client error (4xx)');
} else if (error.isServerError) {
console.log('Server error (5xx)');
} else if (error.isNetworkError) {
console.log('Network error (no response)');
}
// Check if retryable
if (error.isRetryable) {
console.log('This error can be retried');
}
}
}
class HttpError {
message: string; // Error message
status?: number; // HTTP status code
response?: Response; // Original Response object
config?: RequestConfig; // Request configuration
isNetworkError: boolean; // No response received
isClientError: boolean; // 4xx status
isServerError: boolean; // 5xx status
isRetryable: boolean; // Can be retried
}
import { describe, test, expect } from 'bun:test';
import { createMockClient } from '@brika/http/testing';
import type { User } from './types';
describe('UserService', () => {
test('should fetch user by id', async () => {
const client = createMockClient();
// Mock a response
client.mockResponse('GET', '/users/123', {
data: { id: '123', name: 'John Doe' },
status: 200,
statusText: 'OK',
headers: new Headers(),
config: { method: 'GET', url: '/users/123' },
cached: false,
});
// Make request
const response = await client.get<User>('/users/123').send();
// Assertions
expect(response.data.name).toBe('John Doe');
expect(response.status).toBe(200);
// Verify request was made
const requests = client.getRequests();
expect(requests).toHaveLength(1);
expect(requests[0].config.method).toBe('GET');
expect(requests[0].config.url).toBe('/users/123');
});
test('should handle errors', async () => {
const client = createMockClient();
client.mockResponse('GET', '/users/999', {
data: { error: 'Not found' },
status: 404,
statusText: 'Not Found',
headers: new Headers(),
config: { method: 'GET', url: '/users/999' },
cached: false,
});
await expect(async () => {
await client.get('/users/999').send();
}).toThrow();
});
});
import { createMockFetch } from '@brika/http/testing';
const mockFetch = createMockFetch();
// Mock specific endpoint
mockFetch.mock(
{ method: 'GET', url: '/users' },
{ status: 200, data: [{ id: '1', name: 'John' }] }
);
// Mock with delay
mockFetch.mock(
{ method: 'POST', url: '/users' },
{ status: 201, data: { id: '2' }, delay: 100 }
);
// Use the mock
const fetchFn = mockFetch.getFetchFn();
const response = await fetchFn('/users');
With caching enabled, repeated requests are significantly faster:
// Frequently changing data
.cache({ ttl: 30_000 }) // 30 seconds
// Moderately stable data
.cache({ ttl: 300_000 }) // 5 minutes
// Stable reference data
.cache({ ttl: 3600_000 }) // 1 hour
// Very stable data
.cache({ ttl: 86400_000 }) // 24 hours
.data() instead of .send() when you only need the response dataBefore:
const url = new URL('https://api.example.com/users');
url.searchParams.set('limit', '10');
url.searchParams.set('page', '1');
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': 'Bearer token',
'Accept': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
After:
const data = await this.#http
.get<User[]>('https://api.example.com/users')
.params({ limit: '10', page: '1' })
.header('Authorization', 'Bearer token')
.cache({ ttl: 60_000 })
.data();
Benefits:
import { HttpClient } from '@brika/http';
import { singleton, inject } from '@brika/di';
interface NpmSearchResult {
objects: Array<{
package: {
name: string;
version: string;
description?: string;
};
}>;
total: number;
}
@singleton()
class NpmSearchService {
readonly #http = inject(HttpClient);
async search(query: string, limit = 20) {
const data = await this.#http
.get<NpmSearchResult>('https://registry.npmjs.org/-/v1/search')
.params({ text: query, size: String(limit) })
.cache({ ttl: 300_000, tags: ['npm-search'] }) // 5 min cache
.data();
return data.objects.map(obj => obj.package);
}
async getPackage(name: string) {
return this.#http
.get(`https://registry.npmjs.org/${name}`)
.cache({ ttl: 600_000, tags: ['npm-package'] }) // 10 min cache
.data();
}
}
import { HttpClient } from '@brika/http';
class ApiClient {
#http: HttpClient;
#token: string | null = null;
constructor() {
this.#http = new HttpClient({
baseUrl: 'https://api.example.com',
interceptors: {
request: [(config) => {
if (this.#token) {
return {
...config,
headers: {
...config.headers,
'Authorization': `Bearer ${this.#token}`,
},
};
}
return config;
}],
error: [async (error, config) => {
if (error.status === 401) {
// Token expired, refresh it
this.#token = await this.refreshToken();
// Retry the request
return this.#http.execute(config);
}
throw error;
}],
},
});
}
async login(email: string, password: string) {
const response = await this.#http
.post<{ token: string }>('/auth/login')
.json({ email, password })
.send();
this.#token = response.data.token;
return response.data;
}
async refreshToken(): Promise<string> {
const response = await this.#http
.post<{ token: string }>('/auth/refresh')
.send();
return response.data.token;
}
}
This package is part of the Brika monorepo. To contribute:
bun testbun run typecheckMIT
Built with ❤️ for Brika
FAQs
Unknown package
We found that @brika/http demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.