Skip to content

Commit e752a6d

Browse files
authored
feat(user): add create-user domain types and API client (#405)
1 parent 9e82cba commit e752a6d

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createUserApi } from "./user-api.ts";
2+
3+
const apiBase = import.meta.env.VITE_API_BASE_URL;
4+
5+
if (!apiBase) {
6+
throw new Error("VITE_API_BASE_URL is not set");
7+
}
8+
9+
const userApi = createUserApi({ apiBase });
10+
11+
export const createUser = userApi.createUser;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { AgeRange, SubscriptionTier } from "../types";
2+
3+
export type UserApiDeps = {
4+
apiBase: string;
5+
fetchImpl?: typeof fetch;
6+
};
7+
8+
export type CreateUserRequest = {
9+
first_name: string;
10+
last_name: string;
11+
email: string;
12+
password: string;
13+
age_range: AgeRange;
14+
subscription_tier: SubscriptionTier;
15+
subscription_expires_at: string | null;
16+
};
17+
18+
export type ApiUserDto = {
19+
id: number;
20+
first_name: string;
21+
last_name: string;
22+
email: string;
23+
age_range: AgeRange;
24+
subscription_tier: SubscriptionTier;
25+
subscription_expires_at: string | null;
26+
};
27+
28+
export type ApiCreateUserResponse =
29+
| { status: "success"; data: ApiUserDto }
30+
| { status: "error"; data: unknown };
31+
32+
const normalizeApiBase = (apiBase: string): string => apiBase.replace(/\/+$/, "");
33+
34+
export const createUserApi = ({ apiBase, fetchImpl = fetch }: UserApiDeps) => {
35+
if (!apiBase) {
36+
throw new Error("apiBase is required");
37+
}
38+
39+
const base = normalizeApiBase(apiBase);
40+
const endpoint = `${base}/api/v1/users`;
41+
42+
const withCommonInit = (init?: RequestInit): RequestInit => ({
43+
...init,
44+
method: "POST",
45+
headers: {
46+
Accept: "application/json",
47+
"Content-Type": "application/json",
48+
...(init?.headers ?? {}),
49+
},
50+
credentials: init?.credentials ?? "omit",
51+
signal: init?.signal,
52+
});
53+
54+
return {
55+
async createUser(request: CreateUserRequest, init?: RequestInit): Promise<ApiUserDto> {
56+
const response = await fetchImpl(endpoint, {
57+
...withCommonInit(init),
58+
body: JSON.stringify(request),
59+
});
60+
61+
if (!response.ok) {
62+
throw new Error(`HTTP ${response.status}`);
63+
}
64+
65+
const json = (await response.json()) as ApiCreateUserResponse;
66+
if (json.status !== "success") {
67+
throw new Error(`API status is not success: ${json.status}`);
68+
}
69+
70+
return json.data;
71+
},
72+
};
73+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const AGE_RANGES = ["teens", "20s", "30s", "40s", "50s", "60plus"] as const;
2+
export type AgeRange = (typeof AGE_RANGES)[number];
3+
4+
export const SUBSCRIPTION_TIERS = ["free", "paid"] as const;
5+
export type SubscriptionTier = (typeof SUBSCRIPTION_TIERS)[number];
6+
7+
export type CreateUserFormValues = {
8+
firstName: string;
9+
lastName: string;
10+
email: string;
11+
password: string;
12+
ageRange: AgeRange;
13+
subscriptionTier: SubscriptionTier;
14+
subscriptionExpiresAt: string | null;
15+
};

0 commit comments

Comments
 (0)