Skip to content

Instantly share code, notes, and snippets.

@ilham25
Created February 4, 2025 13:01
Show Gist options
  • Save ilham25/670ee0ce0d6b4bdd5fba621c7d5c6d7b to your computer and use it in GitHub Desktop.
Save ilham25/670ee0ce0d6b4bdd5fba621c7d5c6d7b to your computer and use it in GitHub Desktop.
Iruha's basic web dev structure

Basic Stack

Project Structure

  • /app This directory is used to route Expo apps. In this example, we use a user dashboard project that has a dashboard page for users, a create user page, and an update user page. Here are the routes:

    • /app/users
      • /app/users/(user-dashboard)
      • /app/users/new
      • /app/users/[userId]

    Usually, the root menu of each route or feature will be grouped (such as (user-dashboard)) to isolate it from its child routes.

  • /components This directory is used to store global components that can be used anywhere in the project.

  • /features This directory is used to store each feature in applications, such as authentication, profile, etc. Each feature usually filled with directory below:

    • /components This directory is used to store any components related to the feature.
    • /components/ui This directory is used to store any global components specific to the related feature, usually used to store compound components for each feature. Compound components reference
    • /hooks This directory is used to store any hooks related to the feature.
    • /stores This directory is used to store zustand store related to the feature.
    • /validators This directory is used to store any validators related to the feature.
    • /utils This directory is used to store any utilities related to the feature.
  • /hooks This directory is used to store global hooks that can be used anywhere in the project.

  • /lib This directory is used to store global libraries, such as API instances.

  • /stores This directory is used to store zustand store that globally used.

  • /services This directory is used to store any API services for each endpoint. For each service, it includes the entity, DTO, query key (for @tanstack/query), and query + mutation hooks.

    • /services/*/dto.ts This directory is used to store DTOs related to the service using Zod. For example:

      // Zod DTO
      export const createUserDto = z.object({
        firstName: z.string(),
        lastName: z.string().optional(),
      });
      // Typing for Zod DTO above
      export type CreateUserDto = z.infer<typeof createUserDto>;
    • /services/*/entity.ts This directory is used to store entities related to the service using Zod. For example:

      export const userEntity = z.object({
        firstName: z.string(),
        lastName: z.string(),
        age: z.number(),
      });
      export type UserEntity = z.infer<typeof userEntity>;
    • /services/*/query-key.factory.ts This directory is used to store query keys related to the service that extend from the shared query key factory at /services/shared/query-key.factory.ts. For example:

      export class UserQueryKeyFactory extends QueryKeyFactory {}
    • /services/*/hooks This directory is used to store any React hooks related to querying and mutating a service. Usually, there are 5 base hooks. For example, for a User service:

      • useFindUser: This hook is used to query all users, with or without a filter.
      • useGetUser: This hook is used to query a user by id.
      • useCreateUser: This hook is used to create a user.
      • useUpdateUser: This hook is used to update a user.
      • useDeleteUser: This hook is used to delete a user.

      Query hooks usually return useQuery hooks from @tanstack/query (e.g., useFindUser and useGetUser), and mutation hooks usually return useMutation hooks from @tanstack/query (e.g., useCreateUser, useUpdateUser, and useDeleteUser). Feel free to add any hooks for other actions.

Git Rules

  1. Maximum 20 file changes (except assets)

    If your PR is more than 20 file changes, please reduce it to smaller PRs.

  2. use a simple self explain branch name, example for creating user profile page UI: feat/user-profile-ui, another example if you want to integrate api with the UI: feat/user-profile-integration

  3. PR title simple self explain of feature in said PR

  4. PR description must include screenshot when slicing UI

Git Commit Message

  • example feature: "feat(home/dashboard): initial UI for dashboard"
  • example chore: "chore(dashboard/navigation): improve code or logic ..."
  • example fix bug: "fix(...): fix missing logic ..."
  • example styling: "style(...): create a navigation bar"
  • example refactor: "refactor(...): refactor missing logic"

Other

  • use kebab-case for file and folder naming (e.g., this-file-naming.ts)
  • do not use export default ... when exporting component, use regular export instead (e.g., export const SomeComponent = () => {...})
// api.ts
import type { AxiosInstance } from "axios";
import { stringify } from "qs";

import { api } from "@/lib/api";

import type {
  BaseFindConnectionResponse,
  BaseFindOneRecordDto,
  BaseFindRecordsDto,
  InfiniteRecord,
} from "./types";

export class BaseApi<Entity> {
  api: AxiosInstance = api;
  endpoint: string;

  constructor(endpoint: string) {
    this.endpoint = endpoint;
  }

  async findAll(dto?: BaseFindRecordsDto<Entity>): Promise<Entity[]> {
  ...
  }

  async findAllConnection(
    dto: BaseFindRecordsDto<Entity>
  ): Promise<BaseFindConnectionResponse<Entity[]>> {
   ...
  }

  async findAllConnectionInfinite(
    dto: BaseFindRecordsDto<Entity>,
    pageParam: number
  ): Promise<InfiniteRecord<Entity>> {
    ...
  }

  async findOne(
    id: string,
    dto?: BaseFindOneRecordDto<Entity>
  ): Promise<Entity> {
  ...
  }

  async create<Dto>(dto: Dto): Promise<Entity> {
    ...
  }

  async update<Dto>(id: string, dto: Dto): Promise<Entity> {
    ...
  }

}
// /services/shared/query-key-factory.ts

/**
 * base query key factory
 **/

import { type QueryKey } from "@tanstack/react-query";
import { stringify } from "qs";

export class QueryKeyFactory {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  all(): QueryKey {
    return [this.name];
  }

  lists(): QueryKey {
    return [...this.all(), "list"];
  }

  list(filters?: object): QueryKey {
    return [
      ...this.lists(),
      { filters: stringify(filters, { encode: false }) },
    ];
  }

  details(): QueryKey {
    return [...this.all(), "detail"];
  }

  detail(id: string, filters?: object): QueryKey {
    if (filters)
      return [
        ...this.details(),
        id,
        { filters: stringify(filters, { encode: false }) },
      ];

    return [...this.details(), id];
  }
}
// /services/analytics/query-key-factory.ts

/**
 * Each service will have query key factory like this, for example for analytics service
 **/

import { type QueryKey } from "@tanstack/react-query";
import { stringify } from "qs";

import { QueryKeyFactory } from "../shared/query-key-factory";

export class AnalyticsQueryKeyFactory extends QueryKeyFactory {
  constructor() {
    super("analytics");
  }

  summaries(): QueryKey {
    return [...this.all(), "summaries"];
  }

  summary(dto: object): QueryKey {
    return [...this.summaries(), { dto: stringify(dto, { encode: false }) }];
  }
}

export const analyticsQueryKeyFactory = new AnalyticsQueryKeyFactory();
// use-base-create-record.ts

"use client";

import type { UseMutationOptions } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";

import { BaseApi } from "../api";
import { queryKeyFactory } from "../query-key.factory";

export const useBaseCreateRecord = <Entity, Dto>(
  endpoint: string,
  options?: UseMutationOptions<Entity, Error, Dto>
) => {
  const baseApi = new BaseApi<Entity>(endpoint);

  return useMutation<Entity, Error, Dto>({
    mutationFn: (dto) => baseApi.create<Dto>(dto),
    mutationKey: queryKeyFactory.create(),
    ...options,
  });
};
// use-base-find-record.ts
"use client";

import type { UseQueryOptions } from "@tanstack/react-query";
import { useQuery } from "@tanstack/react-query";

import { BaseApi } from "../api";
import { queryKeyFactory } from "../query-key.factory";
import type { BaseFindRecordsDto } from "../types";

export const useBaseFindRecord = <T>(
  endpoint: string,
  payload: { dto?: BaseFindRecordsDto<T> },
  options?: Omit<UseQueryOptions<T[]>, "initialData" | "queryKey">
) => {
  const baseApi = new BaseApi<T>(endpoint);

  return useQuery<T[]>({
    queryKey: queryKeyFactory.list(payload.dto),
    queryFn: () => baseApi.findAll(payload.dto),
    initialData: [],
    ...options,
  });
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment