@yoms/create-monorepo 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +132 -4
  2. package/dist/index.js +113 -4
  3. package/package.json +1 -1
  4. package/templates/backend-hono/base/{AGENT.md → CLAUDE.md} +42 -3
  5. package/templates/backend-hono/features/better-auth/env-additions.txt +4 -0
  6. package/templates/backend-hono/features/better-auth/package-additions.json +5 -0
  7. package/templates/backend-hono/features/better-auth/prisma/schema.prisma +69 -0
  8. package/templates/backend-hono/features/better-auth/src/config/env-auth.ts +8 -0
  9. package/templates/backend-hono/features/better-auth/src/index.ts +53 -0
  10. package/templates/backend-hono/features/better-auth/src/lib/auth.ts +21 -0
  11. package/templates/backend-hono/features/better-auth/src/middleware/auth.middleware.ts +38 -0
  12. package/templates/frontend-nextjs/base/CLAUDE.md +183 -0
  13. package/templates/frontend-nextjs/base/app/layout.tsx +4 -1
  14. package/templates/frontend-nextjs/base/components/examples/users-list-example.tsx +127 -0
  15. package/templates/frontend-nextjs/base/lib/auth-client.ts +18 -0
  16. package/templates/frontend-nextjs/base/package.json +4 -1
  17. package/templates/frontend-nextjs/base/providers/query-provider.tsx +28 -0
  18. package/templates/frontend-nextjs/base/services/README.md +184 -0
  19. package/templates/frontend-nextjs/base/{lib/api-client.ts → services/api/client.ts} +1 -0
  20. package/templates/frontend-nextjs/base/services/api/endpoints.ts +26 -0
  21. package/templates/frontend-nextjs/base/services/api/index.ts +6 -0
  22. package/templates/frontend-nextjs/base/services/auth/auth.hook.ts +61 -0
  23. package/templates/frontend-nextjs/base/services/auth/auth.types.ts +38 -0
  24. package/templates/frontend-nextjs/base/services/auth/index.ts +12 -0
  25. package/templates/frontend-nextjs/base/services/users/index.ts +8 -0
  26. package/templates/frontend-nextjs/base/services/users/users.hook.ts +119 -0
  27. package/templates/frontend-nextjs/base/services/users/users.queries.ts +14 -0
  28. package/templates/frontend-nextjs/base/services/users/users.service.ts +65 -0
  29. package/templates/frontend-nextjs/base/services/users/users.types.ts +37 -0
  30. package/templates/shared/base/CLAUDE.md +95 -0
  31. package/templates/backend-hono/features/jwt-auth/env-additions.txt +0 -5
  32. package/templates/backend-hono/features/jwt-auth/package-additions.json +0 -10
  33. package/templates/backend-hono/features/jwt-auth/src/config/env-additions.ts +0 -16
  34. package/templates/backend-hono/features/jwt-auth/src/lib/jwt.ts +0 -75
  35. package/templates/backend-hono/features/jwt-auth/src/middleware/auth.middleware.ts +0 -50
  36. package/templates/backend-hono/features/jwt-auth/src/routes/auth.route.ts +0 -157
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Auth types — aligned with better-auth's session model.
3
+ */
4
+
5
+ export interface AuthUser {
6
+ id: string;
7
+ name: string;
8
+ email: string;
9
+ emailVerified: boolean;
10
+ image?: string | null;
11
+ createdAt: Date;
12
+ updatedAt: Date;
13
+ }
14
+
15
+ export interface AuthSession {
16
+ user: AuthUser;
17
+ session: {
18
+ id: string;
19
+ userId: string;
20
+ expiresAt: Date;
21
+ token: string;
22
+ ipAddress?: string | null;
23
+ userAgent?: string | null;
24
+ };
25
+ }
26
+
27
+ export interface SignInInput {
28
+ email: string;
29
+ password: string;
30
+ callbackURL?: string;
31
+ }
32
+
33
+ export interface SignUpInput {
34
+ name: string;
35
+ email: string;
36
+ password: string;
37
+ callbackURL?: string;
38
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Auth service exports
3
+ *
4
+ * Quick start:
5
+ * import { useSession, useSignIn, useSignUp, useSignOut } from '@/services/auth';
6
+ *
7
+ * For direct better-auth client access:
8
+ * import { authClient } from '@/lib/auth-client';
9
+ */
10
+
11
+ export * from './auth.types';
12
+ export * from './auth.hook';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Users service exports
3
+ */
4
+
5
+ export * from './users.types';
6
+ export * from './users.service';
7
+ export * from './users.queries';
8
+ export * from './users.hook';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * User React Query hooks
3
+ * Custom hooks that wrap user service calls with React Query
4
+ */
5
+
6
+ import {
7
+ useQuery,
8
+ useMutation,
9
+ useQueryClient,
10
+ type UseQueryOptions,
11
+ type UseMutationOptions,
12
+ } from '@tanstack/react-query';
13
+ import { UserService } from './users.service';
14
+ import { userKeys } from './users.queries';
15
+ import type {
16
+ User,
17
+ PaginatedUsers,
18
+ CreateUserInput,
19
+ UpdateUserInput,
20
+ UsersListParams,
21
+ } from './users.types';
22
+ import type { ApiResponse } from '__PACKAGE_SCOPE__/shared';
23
+
24
+ /**
25
+ * Hook to fetch paginated users list
26
+ */
27
+ export function useUsers(
28
+ params?: UsersListParams,
29
+ options?: Omit<
30
+ UseQueryOptions<ApiResponse<PaginatedUsers>>,
31
+ 'queryKey' | 'queryFn'
32
+ >
33
+ ) {
34
+ return useQuery({
35
+ queryKey: userKeys.list(params),
36
+ queryFn: () => UserService.getUsers(params),
37
+ ...options,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Hook to fetch a single user by ID
43
+ */
44
+ export function useUser(
45
+ id: string,
46
+ options?: Omit<UseQueryOptions<ApiResponse<User>>, 'queryKey' | 'queryFn'>
47
+ ) {
48
+ return useQuery({
49
+ queryKey: userKeys.detail(id),
50
+ queryFn: () => UserService.getUserById(id),
51
+ enabled: !!id,
52
+ ...options,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Hook to create a new user
58
+ */
59
+ export function useCreateUser(
60
+ options?: UseMutationOptions<
61
+ ApiResponse<User>,
62
+ Error,
63
+ CreateUserInput
64
+ >
65
+ ) {
66
+ const queryClient = useQueryClient();
67
+
68
+ return useMutation({
69
+ mutationFn: (data: CreateUserInput) => UserService.createUser(data),
70
+ onSuccess: () => {
71
+ // Invalidate users list to refetch after creation
72
+ queryClient.invalidateQueries({ queryKey: userKeys.lists() });
73
+ },
74
+ ...options,
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Hook to update an existing user
80
+ */
81
+ export function useUpdateUser(
82
+ options?: UseMutationOptions<
83
+ ApiResponse<User>,
84
+ Error,
85
+ { id: string; data: UpdateUserInput }
86
+ >
87
+ ) {
88
+ const queryClient = useQueryClient();
89
+
90
+ return useMutation({
91
+ mutationFn: ({ id, data }: { id: string; data: UpdateUserInput }) =>
92
+ UserService.updateUser(id, data),
93
+ onSuccess: (_, variables) => {
94
+ // Invalidate both the specific user and the users list
95
+ queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
96
+ queryClient.invalidateQueries({ queryKey: userKeys.lists() });
97
+ },
98
+ ...options,
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Hook to delete a user
104
+ */
105
+ export function useDeleteUser(
106
+ options?: UseMutationOptions<ApiResponse<void>, Error, string>
107
+ ) {
108
+ const queryClient = useQueryClient();
109
+
110
+ return useMutation({
111
+ mutationFn: (id: string) => UserService.deleteUser(id),
112
+ onSuccess: (_, id) => {
113
+ // Remove the deleted user from cache and invalidate lists
114
+ queryClient.removeQueries({ queryKey: userKeys.detail(id) });
115
+ queryClient.invalidateQueries({ queryKey: userKeys.lists() });
116
+ },
117
+ ...options,
118
+ });
119
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * User query key factory
3
+ * Provides type-safe query keys for React Query
4
+ */
5
+
6
+ import type { UsersListParams } from './users.types';
7
+
8
+ export const userKeys = {
9
+ all: ['users'] as const,
10
+ lists: () => [...userKeys.all, 'list'] as const,
11
+ list: (params?: UsersListParams) => [...userKeys.lists(), params] as const,
12
+ details: () => [...userKeys.all, 'detail'] as const,
13
+ detail: (id: string) => [...userKeys.details(), id] as const,
14
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * User service
3
+ * Service layer for user-related API calls
4
+ */
5
+
6
+ import { ApiClient } from '../api/client';
7
+ import { API_ENDPOINTS } from '../api/endpoints';
8
+ import type {
9
+ User,
10
+ PaginatedUsers,
11
+ CreateUserInput,
12
+ UpdateUserInput,
13
+ UsersListParams,
14
+ } from './users.types';
15
+ import type { ApiResponse } from '__PACKAGE_SCOPE__/shared';
16
+
17
+ export class UserService {
18
+ /**
19
+ * Fetch paginated list of users
20
+ */
21
+ static async getUsers(
22
+ params?: UsersListParams
23
+ ): Promise<ApiResponse<PaginatedUsers>> {
24
+ const searchParams = new URLSearchParams();
25
+ if (params?.page) searchParams.set('page', params.page.toString());
26
+ if (params?.limit) searchParams.set('limit', params.limit.toString());
27
+ if (params?.search) searchParams.set('search', params.search);
28
+
29
+ const query = searchParams.toString();
30
+ const endpoint = query ? `${API_ENDPOINTS.users.list}?${query}` : API_ENDPOINTS.users.list;
31
+
32
+ return ApiClient.get<PaginatedUsers>(endpoint);
33
+ }
34
+
35
+ /**
36
+ * Fetch a single user by ID
37
+ */
38
+ static async getUserById(id: string): Promise<ApiResponse<User>> {
39
+ return ApiClient.get<User>(API_ENDPOINTS.users.byId(id));
40
+ }
41
+
42
+ /**
43
+ * Create a new user
44
+ */
45
+ static async createUser(data: CreateUserInput): Promise<ApiResponse<User>> {
46
+ return ApiClient.post<User>(API_ENDPOINTS.users.create, data);
47
+ }
48
+
49
+ /**
50
+ * Update an existing user
51
+ */
52
+ static async updateUser(
53
+ id: string,
54
+ data: UpdateUserInput
55
+ ): Promise<ApiResponse<User>> {
56
+ return ApiClient.put<User>(API_ENDPOINTS.users.update(id), data);
57
+ }
58
+
59
+ /**
60
+ * Delete a user
61
+ */
62
+ static async deleteUser(id: string): Promise<ApiResponse<void>> {
63
+ return ApiClient.delete<void>(API_ENDPOINTS.users.delete(id));
64
+ }
65
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * User service types
3
+ */
4
+
5
+ export interface User {
6
+ id: string;
7
+ name: string;
8
+ email: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ }
12
+
13
+ export interface CreateUserInput {
14
+ name: string;
15
+ email: string;
16
+ }
17
+
18
+ export interface UpdateUserInput {
19
+ name?: string;
20
+ email?: string;
21
+ }
22
+
23
+ export interface UsersListParams {
24
+ page?: number;
25
+ limit?: number;
26
+ search?: string;
27
+ }
28
+
29
+ export interface PaginatedUsers {
30
+ data: User[];
31
+ meta: {
32
+ total: number;
33
+ page: number;
34
+ limit: number;
35
+ totalPages: number;
36
+ };
37
+ }
@@ -0,0 +1,95 @@
1
+ # CLAUDE.md — Shared Package (`__PACKAGE_SCOPE__/shared`)
2
+
3
+ This file provides guidance to Claude Code when working in this package.
4
+
5
+ ## Overview
6
+
7
+ Shared TypeScript types and Zod schemas used by both the backend API and frontend web app. This is the single source of truth for data shapes across the monorepo.
8
+
9
+ ## Structure
10
+
11
+ ```
12
+ src/
13
+ ├── index.ts # Barrel export — everything exported from here
14
+ ├── types.ts # Shared TypeScript interfaces (ApiResponse, PaginatedResponse)
15
+ └── schemas/
16
+ └── user.schema.ts # Zod schemas + inferred types for User domain
17
+ ```
18
+
19
+ ## Development Commands
20
+
21
+ ```bash
22
+ pnpm build # Build (required before other packages can consume it)
23
+ pnpm typecheck # Type check
24
+ ```
25
+
26
+ > **Important**: Run `pnpm build` after changes so backend and frontend pick up the latest types.
27
+
28
+ ## Adding New Schemas
29
+
30
+ Create a new file in `src/schemas/`:
31
+
32
+ ```typescript
33
+ // src/schemas/post.schema.ts
34
+ import { z } from 'zod';
35
+
36
+ export const PostSchema = z.object({
37
+ id: z.string(),
38
+ title: z.string(),
39
+ content: z.string().nullable(),
40
+ authorId: z.string(),
41
+ createdAt: z.coerce.date(),
42
+ updatedAt: z.coerce.date(),
43
+ });
44
+ export type Post = z.infer<typeof PostSchema>;
45
+
46
+ export const CreatePostSchema = z.object({
47
+ title: z.string().min(1),
48
+ content: z.string().optional(),
49
+ authorId: z.string(),
50
+ });
51
+ export type CreatePost = z.infer<typeof CreatePostSchema>;
52
+
53
+ export const UpdatePostSchema = CreatePostSchema.partial();
54
+ export type UpdatePost = z.infer<typeof UpdatePostSchema>;
55
+ ```
56
+
57
+ Then re-export from `src/index.ts`:
58
+
59
+ ```typescript
60
+ export * from './schemas/post.schema.js';
61
+ ```
62
+
63
+ ## Consuming in Backend
64
+
65
+ ```typescript
66
+ import { PostSchema, type CreatePost } from '__PACKAGE_SCOPE__/shared';
67
+ import { zValidator } from '@hono/zod-validator';
68
+
69
+ posts.post('/', zValidator('json', CreatePostSchema), async (c) => {
70
+ const data = c.req.valid('json'); // typed as CreatePost
71
+ });
72
+ ```
73
+
74
+ ## Consuming in Frontend
75
+
76
+ ```typescript
77
+ import type { Post, CreatePost } from '__PACKAGE_SCOPE__/shared';
78
+ import { PostSchema } from '__PACKAGE_SCOPE__/shared';
79
+
80
+ // In service
81
+ export class PostService {
82
+ static async getAll(): Promise<ApiResponse<Post[]>> {
83
+ return ApiClient.get('/posts');
84
+ }
85
+ }
86
+ ```
87
+
88
+ ## Rules
89
+
90
+ - Only pure TypeScript / Zod — no framework-specific code
91
+ - No runtime dependencies other than `zod`
92
+ - Always export both the Zod schema AND the inferred TypeScript type
93
+ - Schema names: `UserSchema` / Type names: `User` (no suffix)
94
+ - Use `.nullable()` not `.optional()` for fields that can be null in the DB
95
+ - Use `.coerce.date()` for `DateTime` fields from Prisma
@@ -1,5 +0,0 @@
1
- # JWT Configuration
2
- JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
3
- JWT_EXPIRES_IN=15m
4
- JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-this-in-production
5
- JWT_REFRESH_EXPIRES_IN=7d
@@ -1,10 +0,0 @@
1
- {
2
- "dependencies": {
3
- "jsonwebtoken": "^9.0.2",
4
- "bcryptjs": "^2.4.3"
5
- },
6
- "devDependencies": {
7
- "@types/jsonwebtoken": "^9.0.7",
8
- "@types/bcryptjs": "^2.4.6"
9
- }
10
- }
@@ -1,16 +0,0 @@
1
- // Add these to your env schema validation
2
-
3
- import { z } from 'zod';
4
-
5
- export const jwtEnvSchema = z.object({
6
- JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
7
- JWT_EXPIRES_IN: z.string().default('15m'),
8
- JWT_REFRESH_SECRET: z.string().min(32, 'JWT_REFRESH_SECRET must be at least 32 characters'),
9
- JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
10
- });
11
-
12
- // Merge this with your existing env schema:
13
- // const envSchema = z.object({
14
- // ...existingFields,
15
- // ...jwtEnvSchema.shape,
16
- // });
@@ -1,75 +0,0 @@
1
- import jwt from 'jsonwebtoken';
2
- import { env } from '../config/env.js';
3
- import { UnauthorizedError } from '../lib/errors.js';
4
-
5
- export interface JwtPayload {
6
- userId: string;
7
- email: string;
8
- type: 'access' | 'refresh';
9
- }
10
-
11
- export interface TokenPair {
12
- accessToken: string;
13
- refreshToken: string;
14
- }
15
-
16
- /**
17
- * Generate access and refresh tokens for a user
18
- */
19
- export function generateTokens(userId: string, email: string): TokenPair {
20
- const accessToken = jwt.sign(
21
- { userId, email, type: 'access' } as JwtPayload,
22
- env.JWT_SECRET,
23
- { expiresIn: env.JWT_EXPIRES_IN }
24
- );
25
-
26
- const refreshToken = jwt.sign(
27
- { userId, email, type: 'refresh' } as JwtPayload,
28
- env.JWT_REFRESH_SECRET,
29
- { expiresIn: env.JWT_REFRESH_EXPIRES_IN }
30
- );
31
-
32
- return { accessToken, refreshToken };
33
- }
34
-
35
- /**
36
- * Verify and decode an access token
37
- */
38
- export function verifyAccessToken(token: string): JwtPayload {
39
- try {
40
- const decoded = jwt.verify(token, env.JWT_SECRET) as JwtPayload;
41
- if (decoded.type !== 'access') {
42
- throw new UnauthorizedError('Invalid token type');
43
- }
44
- return decoded;
45
- } catch (error) {
46
- if (error instanceof jwt.TokenExpiredError) {
47
- throw new UnauthorizedError('Token expired');
48
- }
49
- if (error instanceof jwt.JsonWebTokenError) {
50
- throw new UnauthorizedError('Invalid token');
51
- }
52
- throw error;
53
- }
54
- }
55
-
56
- /**
57
- * Verify and decode a refresh token
58
- */
59
- export function verifyRefreshToken(token: string): JwtPayload {
60
- try {
61
- const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET) as JwtPayload;
62
- if (decoded.type !== 'refresh') {
63
- throw new UnauthorizedError('Invalid token type');
64
- }
65
- return decoded;
66
- } catch (error) {
67
- if (error instanceof jwt.TokenExpiredError) {
68
- throw new UnauthorizedError('Refresh token expired');
69
- }
70
- if (error instanceof jwt.JsonWebTokenError) {
71
- throw new UnauthorizedError('Invalid refresh token');
72
- }
73
- throw error;
74
- }
75
- }
@@ -1,50 +0,0 @@
1
- import type { Context, Next } from 'hono';
2
- import { verifyAccessToken } from '../lib/jwt.js';
3
- import { UnauthorizedError } from '../lib/errors.js';
4
-
5
- /**
6
- * Middleware to authenticate requests using JWT
7
- * Extracts and verifies the JWT token from the Authorization header
8
- * and attaches the decoded payload to c.get('jwtPayload')
9
- */
10
- export async function authMiddleware(c: Context, next: Next): Promise<void> {
11
- const authHeader = c.req.header('Authorization');
12
-
13
- if (!authHeader) {
14
- throw new UnauthorizedError('No authorization header');
15
- }
16
-
17
- const [type, token] = authHeader.split(' ');
18
-
19
- if (type !== 'Bearer' || !token) {
20
- throw new UnauthorizedError('Invalid authorization header format');
21
- }
22
-
23
- const payload = verifyAccessToken(token);
24
- c.set('jwtPayload', payload);
25
-
26
- await next();
27
- }
28
-
29
- /**
30
- * Optional authentication middleware
31
- * Similar to authMiddleware but doesn't throw if no token is present
32
- * Use this for routes that work for both authenticated and unauthenticated users
33
- */
34
- export async function optionalAuthMiddleware(c: Context, next: Next): Promise<void> {
35
- const authHeader = c.req.header('Authorization');
36
-
37
- if (authHeader) {
38
- try {
39
- const [type, token] = authHeader.split(' ');
40
- if (type === 'Bearer' && token) {
41
- const payload = verifyAccessToken(token);
42
- c.set('jwtPayload', payload);
43
- }
44
- } catch {
45
- // Silently ignore invalid tokens for optional auth
46
- }
47
- }
48
-
49
- await next();
50
- }