Appearance
Сквозной сценарий: список постов
Разберём один read-flow целиком:
txt
Axios
→ unknown
→ Valibot
→ DTO
→ dayjs mapper
→ MobX store
→ observer
→ dumb UI
→ pageЦель примера — показать соединение слоёв. В реальном проекте детали loading/error policy могут отличаться.
1. Axios facade
ts
// app/infrastructure/facade/axios/axios.clients.ts
import axios from "axios"
export const api = axios.create({
baseURL: "/api",
})Весь проект использует настроенный client через public API facade.
2. Response schema
ts
// data/posts/posts.schema.ts
import { array, object, string } from "valibot"
export const PostSchema = object({
id: string(),
title: string(),
excerpt: string(),
published_at: string(),
})
export const PostsResponseSchema = object({
posts: array(PostSchema),
})3. DTO из schema
ts
// data/posts/posts.dto.ts
import type { InferOutput } from "valibot"
import { PostSchema, PostsResponseSchema } from "./posts.schema"
export type PostDto = InferOutput<typeof PostSchema>
export type PostsResponseDto = InferOutput<typeof PostsResponseSchema>4. Внутренняя модель
ts
// data/posts/posts.types.ts
export interface IPost {
id: string
title: string
excerpt: string
publishedAt: Date
}5. Mapper с dayjs
ts
// data/posts/posts.mapper.ts
import dayjs from "dayjs"
import type { PostDto } from "./posts.dto"
import type { IPost } from "./posts.types"
export const mapPostDtoToPost = (dto: PostDto): IPost => ({
id: dto.id,
title: dto.title,
excerpt: dto.excerpt,
publishedAt: dayjs(dto.published_at).toDate(),
})6. Domain API
ts
// data/posts/posts.api.ts
import { api } from "@/app/infrastructure/facade/axios"
import { parse } from "valibot"
import { mapPostDtoToPost } from "./posts.mapper"
import { PostsResponseSchema } from "./posts.schema"
import type { IPost } from "./posts.types"
export const getPosts = async (): Promise<IPost[]> => {
const response = await api.get("/posts")
const dto = parse(
PostsResponseSchema,
response.data as unknown,
)
return dto.posts.map(mapPostDtoToPost)
}Axios отвечает за transport. data/posts отвечает за endpoint, validation и domain mapping.
7. MobX store
ts
// data/posts/posts.store.ts
import { makeAutoObservable, runInAction } from "mobx"
import { getPosts } from "./posts.api"
import type { IPost } from "./posts.types"
export class PostsStore {
posts: IPost[] = []
isLoading = false
error: string | null = null
constructor() {
makeAutoObservable(this)
}
get hasPosts(): boolean {
return this.posts.length > 0
}
async load(): Promise<void> {
this.isLoading = true
this.error = null
try {
const posts = await getPosts()
runInAction(() => {
this.posts = posts
})
} catch {
runInAction(() => {
this.error = "Не удалось загрузить посты"
})
} finally {
runInAction(() => {
this.isLoading = false
})
}
}
}
export const postsStore = new PostsStore()HTTP-слой здесь отвечает только за transport. PostsStore по-прежнему владеет состоянием сценария: загрузкой, ошибкой и списком постов.
8. Public API
ts
// data/posts/index.ts
import { PostsStore, postsStore } from "./posts.store"
import type { IPost } from "./posts.types"
export {
PostsStore,
postsStore,
type IPost,
}Schema, DTO, mapper и raw API operation не обязаны быть публичными.
9. Smart React component
tsx
// components/posts/widgets/PostsWidget/PostsWidget.tsx
import { observer } from "mobx-react-lite"
import { postsStore } from "@/data/posts"
import { PostsList } from "../../ui/PostsList"
export const PostsWidget = observer(() => {
if (postsStore.isLoading) {
return <p>Загрузка...</p>
}
if (postsStore.error) {
return <p>{postsStore.error}</p>
}
return <PostsList posts={postsStore.posts} />
})
PostsWidget.displayName = "PostsWidget"10. Dumb UI
tsx
// components/posts/ui/PostsList/PostsList.tsx
import type { IPost } from "@/data/posts"
interface IPostsListProps {
posts: IPost[]
}
export const PostsList = ({ posts }: IPostsListProps) => (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
PostsList.displayName = "PostsList"UI не знает Axios, schema и DTO.
11. Page
tsx
// pages/Posts/Posts.page.tsx
import { PostsWidget } from "@/components/posts"
export const PostsPage = () => <PostsWidget />
PostsPage.displayName = "PostsPage"Где Core, а где Stack
Core:
txt
unknown → Valibot → DTO → mapper → MobX → ReactFull Stack:
txt
Axios facade
dayjs inside mapperЕсли Axios или dayjs заменить, Core-поток остаётся тем же.
См. Example project для полного проекта.