Skip to content

Commit a67fb5f

Browse files
committed
feat: introduce @nbw/api-client package with TypeScript support, Axios integration, and modular service architecture for API interactions
1 parent 4cf745a commit a67fb5f

File tree

12 files changed

+742
-0
lines changed

12 files changed

+742
-0
lines changed

packages/api-client/README.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# @nbw/api-client
2+
3+
A TypeScript API client for the NoteBlockWorld backend API, built with axios.
4+
5+
## Installation
6+
7+
Since this is a workspace package, you can import it in any other package:
8+
9+
```typescript
10+
import { createApiClient, createDefaultConfig } from '@nbw/api-client';
11+
// or
12+
import { NoteBlockWorldApiClient } from '@nbw/api-client';
13+
```
14+
15+
## Usage
16+
17+
### Basic Setup
18+
19+
```typescript
20+
import { createApiClient, createDefaultConfig } from '@nbw/api-client';
21+
22+
// Create a client with default configuration
23+
const client = createApiClient(createDefaultConfig('http://localhost:4000'));
24+
25+
// Or create with custom configuration
26+
const client = createApiClient({
27+
baseURL: 'http://localhost:4000/v1',
28+
timeout: 30000,
29+
headers: {
30+
'Custom-Header': 'value',
31+
},
32+
});
33+
```
34+
35+
### Authentication
36+
37+
```typescript
38+
// Set authentication tokens
39+
client.setAuthTokens({
40+
accessToken: 'your-jwt-token',
41+
refreshToken: 'your-refresh-token',
42+
});
43+
44+
// Clear tokens
45+
client.clearAuthTokens();
46+
47+
// Get current tokens
48+
const tokens = client.getAuthTokens();
49+
```
50+
51+
### Using Services
52+
53+
The API client provides organized services for different modules:
54+
55+
#### Auth Service
56+
57+
```typescript
58+
// Send magic link email
59+
await client.auth.sendMagicLink({ destination: '[email protected]' });
60+
61+
// Verify token
62+
const verification = await client.auth.verifyToken();
63+
64+
// Get OAuth URLs
65+
const githubUrl = client.auth.getGitHubLoginUrl();
66+
const googleUrl = client.auth.getGoogleLoginUrl();
67+
const discordUrl = client.auth.getDiscordLoginUrl();
68+
```
69+
70+
#### User Service
71+
72+
```typescript
73+
// Get current user
74+
const user = await client.user.getMe();
75+
76+
// Get user by email or ID
77+
const user = await client.user.getUser({ email: '[email protected]' });
78+
79+
// Update username
80+
await client.user.updateUsername({ username: 'newusername' });
81+
82+
// Get paginated users
83+
const users = await client.user.getUsersPaginated({ page: 1, limit: 10 });
84+
```
85+
86+
#### Song Service
87+
88+
```typescript
89+
// Get songs with pagination and filtering
90+
const songs = await client.song.getSongs({
91+
page: 1,
92+
limit: 20,
93+
sort: 'createdAt',
94+
order: 'desc',
95+
});
96+
97+
// Get featured songs
98+
const featured = await client.song.getSongs({ q: 'featured' });
99+
100+
// Get recent songs
101+
const recent = await client.song.getSongs({ q: 'recent' });
102+
103+
// Get categories
104+
const categories = await client.song.getSongs({ q: 'categories' });
105+
106+
// Get random songs
107+
const random = await client.song.getSongs({
108+
q: 'random',
109+
count: 5,
110+
category: 'pop',
111+
});
112+
113+
// Search songs
114+
const searchResults = await client.song.searchSongs(
115+
{ page: 1, limit: 10 },
116+
'my search query',
117+
);
118+
119+
// Get specific song
120+
const song = await client.song.getSong('song-id');
121+
122+
// Get song for editing (requires auth)
123+
const editableSong = await client.song.getSongForEdit('song-id');
124+
125+
// Update song (requires auth)
126+
await client.song.updateSong('song-id', songData);
127+
128+
// Upload new song (requires auth)
129+
const file = new File([nbsFileContent], 'song.nbs');
130+
const uploadResult = await client.song.uploadSong(file, {
131+
title: 'My Song',
132+
description: 'A great song',
133+
// ... other song metadata
134+
});
135+
136+
// Get user's songs (requires auth)
137+
const mySongs = await client.song.getMySongs({ page: 1, limit: 10 });
138+
139+
// Delete song (requires auth)
140+
await client.song.deleteSong('song-id');
141+
142+
// Get download URL
143+
const downloadUrl = client.song.getSongDownloadUrl('song-id', 'source');
144+
```
145+
146+
#### Seed Service
147+
148+
```typescript
149+
// Seed development data
150+
await client.seed.seedDev();
151+
```
152+
153+
### Error Handling
154+
155+
All methods return promises that may reject with an `ApiError`:
156+
157+
```typescript
158+
try {
159+
const songs = await client.song.getSongs();
160+
} catch (error) {
161+
console.error('API Error:', error.message);
162+
console.error('Status:', error.status);
163+
console.error('Code:', error.code);
164+
}
165+
```
166+
167+
### Using Individual Services
168+
169+
You can also import and use individual services:
170+
171+
```typescript
172+
import { SongService, createDefaultConfig } from '@nbw/api-client';
173+
174+
const songService = new SongService(
175+
createDefaultConfig('http://localhost:4000'),
176+
);
177+
const songs = await songService.getSongs();
178+
```
179+
180+
### Custom Extensions
181+
182+
Extend the base client for custom functionality:
183+
184+
```typescript
185+
import { BaseApiClient } from '@nbw/api-client';
186+
187+
class CustomApiClient extends BaseApiClient {
188+
async customEndpoint() {
189+
return this.get('/custom-endpoint');
190+
}
191+
}
192+
```
193+
194+
## API Response Format
195+
196+
All API methods return an `ApiResponse<T>` object:
197+
198+
```typescript
199+
interface ApiResponse<T> {
200+
data: T;
201+
status: number;
202+
statusText: string;
203+
headers: Record<string, string>;
204+
}
205+
```
206+
207+
## TypeScript Support
208+
209+
The client is fully typed using the DTOs from `@nbw/database`. All request/response types are properly typed for excellent IDE support and type checking.
210+

packages/api-client/package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@nbw/api-client",
3+
"main": "dist/index.js",
4+
"module": "dist/index.js",
5+
"types": "dist/index.d.ts",
6+
"type": "module",
7+
"private": true,
8+
"exports": {
9+
".": {
10+
"import": "./dist/index.js",
11+
"types": "./dist/index.d.ts",
12+
"development": "./src/index.ts"
13+
}
14+
},
15+
"scripts": {
16+
"build": "bun run clean && bun run build:js && bun run build:types",
17+
"build:js": "tsc --project tsconfig.build.json --declaration false --emitDeclarationOnly false",
18+
"build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly",
19+
"clean": "rm -rf dist",
20+
"dev": "tsc --project tsconfig.build.json --watch",
21+
"lint": "eslint \"src/**/*.ts\" --fix",
22+
"test": "bun test **/*.spec.ts"
23+
},
24+
"devDependencies": {
25+
"@types/bun": "latest",
26+
"typescript": "^5"
27+
},
28+
"dependencies": {
29+
"axios": "^1.6.7",
30+
"@nbw/database": "workspace:*"
31+
},
32+
"peerDependencies": {
33+
"typescript": "^5"
34+
}
35+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { BaseApiClient } from './base-client';
2+
import { AuthService } from './services/auth.service';
3+
import { SeedService } from './services/seed.service';
4+
import { SongService } from './services/song.service';
5+
import { UserService } from './services/user.service';
6+
import type { ApiClientConfig, AuthTokens } from './types';
7+
8+
export class NoteBlockWorldApiClient extends BaseApiClient {
9+
public readonly auth: AuthService;
10+
public readonly user: UserService;
11+
public readonly song: SongService;
12+
public readonly seed: SeedService;
13+
14+
constructor(config: ApiClientConfig) {
15+
super(config);
16+
17+
// Initialize all service instances with the same config
18+
this.auth = new AuthService(config);
19+
this.user = new UserService(config);
20+
this.song = new SongService(config);
21+
this.seed = new SeedService(config);
22+
}
23+
24+
/**
25+
* Set authentication tokens for all services
26+
*/
27+
setAuthTokens(tokens: AuthTokens): void {
28+
super.setAuthTokens(tokens);
29+
this.auth.setAuthTokens(tokens);
30+
this.user.setAuthTokens(tokens);
31+
this.song.setAuthTokens(tokens);
32+
this.seed.setAuthTokens(tokens);
33+
}
34+
35+
/**
36+
* Clear authentication tokens from all services
37+
*/
38+
clearAuthTokens(): void {
39+
super.clearAuthTokens();
40+
this.auth.clearAuthTokens();
41+
this.user.clearAuthTokens();
42+
this.song.clearAuthTokens();
43+
this.seed.clearAuthTokens();
44+
}
45+
46+
/**
47+
* Get authentication tokens
48+
*/
49+
getAuthTokens(): AuthTokens {
50+
return super.getAuthTokens();
51+
}
52+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
2+
3+
import type { ApiClientConfig, AuthTokens, ApiResponse, ApiError } from './types';
4+
5+
export class BaseApiClient {
6+
protected client: AxiosInstance;
7+
private authTokens: AuthTokens = {};
8+
9+
constructor(config: ApiClientConfig) {
10+
this.client = axios.create({
11+
baseURL: config.baseURL,
12+
timeout: config.timeout || 30000,
13+
headers: {
14+
'Content-Type': 'application/json',
15+
...config.headers,
16+
},
17+
});
18+
19+
this.setupInterceptors();
20+
}
21+
22+
private setupInterceptors() {
23+
// Request interceptor to add auth tokens
24+
this.client.interceptors.request.use(
25+
(config) => {
26+
if (this.authTokens.accessToken) {
27+
config.headers.Authorization = `Bearer ${this.authTokens.accessToken}`;
28+
}
29+
return config;
30+
},
31+
(error) => Promise.reject(error)
32+
);
33+
34+
// Response interceptor for error handling
35+
this.client.interceptors.response.use(
36+
(response: AxiosResponse) => response,
37+
(error) => {
38+
const apiError: ApiError = {
39+
message: error.response?.data?.message || error.message || 'An error occurred',
40+
status : error.response?.status,
41+
code : error.response?.data?.code,
42+
};
43+
return Promise.reject(apiError);
44+
}
45+
);
46+
}
47+
48+
setAuthTokens(tokens: AuthTokens) {
49+
this.authTokens = tokens;
50+
}
51+
52+
getAuthTokens(): AuthTokens {
53+
return { ...this.authTokens };
54+
}
55+
56+
clearAuthTokens() {
57+
this.authTokens = {};
58+
}
59+
60+
protected async request<T = any>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
61+
try {
62+
const response = await this.client.request<T>(config);
63+
return {
64+
data : response.data,
65+
status : response.status,
66+
statusText: response.statusText,
67+
headers : response.headers as Record<string, string>,
68+
};
69+
} catch (error) {
70+
throw error;
71+
}
72+
}
73+
74+
protected async get<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
75+
return this.request<T>({ ...config, method: 'GET', url });
76+
}
77+
78+
protected async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
79+
return this.request<T>({ ...config, method: 'POST', url, data });
80+
}
81+
82+
protected async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
83+
return this.request<T>({ ...config, method: 'PUT', url, data });
84+
}
85+
86+
protected async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
87+
return this.request<T>({ ...config, method: 'PATCH', url, data });
88+
}
89+
90+
protected async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
91+
return this.request<T>({ ...config, method: 'DELETE', url });
92+
}
93+
}

0 commit comments

Comments
 (0)