Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
01cfd8e
refactor(articles): reorganize files and add architecture documentation
yamcodes Apr 17, 2025
3903bfe
Refactor articles and comments modules with schema and DTO separation
yamcodes Apr 17, 2025
6562e21
feat: add article mappers and domain interfaces, update architecture …
yamcodes Apr 19, 2025
da9309a
Add project structure docs and fix broken markdown link
yamcodes Apr 19, 2025
3948808
Add architecture diagram showing layer relationships and data flow
yamcodes Apr 19, 2025
d63fe60
refactor: consolidate article mappers and rename ArticleInDb to Artic…
yamcodes Apr 19, 2025
52cff00
Add ArticleDto schema with type validation for article data
yamcodes Apr 19, 2025
673bf6e
Reorder imports alphabetically across multiple files
yamcodes Apr 19, 2025
c75a113
Consolidate article schemas and add body field to article interface
yamcodes Apr 19, 2025
ace8438
Fix import order and reorganize imports in articles schema
yamcodes Apr 19, 2025
07d04ca
refactor: reorganize articles module and improve type safety
yamcodes Apr 19, 2025
87f3064
Refactor articles service to separate query params from options, fix …
yamcodes Apr 19, 2025
f65bb7e
refactor: improve articles feed filtering and add followed users lookup
yamcodes Apr 19, 2025
c3672aa
Refactor article response mapping and add domain model transformation
yamcodes Apr 19, 2025
07f0cb4
Refactor feed endpoint to handle pagination and add article response …
yamcodes Apr 19, 2025
61c7be5
refactor: replace toResponse with toDomain and fix author id access
yamcodes Apr 19, 2025
05a02a6
Add delete article endpoint and improve response handling in article …
yamcodes Apr 19, 2025
c119d2d
Merge branch 'main' into layered
yamcodes Jun 6, 2025
2a0352d
chore: update bun.lockb to reflect dependency changes
yamcodes Jun 6, 2025
86c3daf
chore: update bun.lockb to synchronize with dependency changes
yamcodes Jun 6, 2025
e7e810a
docs: update architecture documentation for clarity and formatting im…
yamcodes Jun 6, 2025
6b0d502
docs: enhance architecture documentation with clearer type convention…
yamcodes Jun 6, 2025
76889ad
refactor: streamline article response structure and enhance service m…
yamcodes Jun 6, 2025
752dff7
refactor: update article response mapping and enhance DTO exports
yamcodes Jun 6, 2025
ffe58a0
refactor: update article imports and enhance comment repository type …
yamcodes Jun 7, 2025
49f999f
refactor: update summary descriptions for articles, tags, and users
yamcodes Jun 7, 2025
7337557
refactor: reorganize article creation endpoint in articles controller
yamcodes Jun 7, 2025
c31c49c
refactor: update article feed query handling and remove unused DTO
yamcodes Jun 7, 2025
5b4fc90
fix: correct spelling of 'Registeration' to 'Registration' in users p…
yamcodes Jun 7, 2025
1fe5c27
feat: enhance articles endpoint description for clarity
yamcodes Jun 7, 2025
e46f93b
refactor: improve articles controller and DTO structure
yamcodes Jun 7, 2025
b4cb3d2
refactor: improve article domain mapping and tag handling
yamcodes Jun 7, 2025
d0fed98
refactor: optimize tag handling and article domain mapping
yamcodes Jun 7, 2025
f4426b0
refactor: update comment response structure and remove article mapping
yamcodes Jun 7, 2025
2cd3563
refactor: improve find method parameter handling in ArticlesRepository
yamcodes Jun 7, 2025
05b3823
refactor: streamline article query handling in ArticlesRepository
yamcodes Jun 7, 2025
64347a2
feat: enhance error handling and article slug generation
yamcodes Jun 7, 2025
8379741
refactor: improve follow/unfollow user functionality in profiles service
yamcodes Jun 7, 2025
031803b
refactor: improve follow/unfollow logic in profiles service
yamcodes Jun 7, 2025
3bae8b0
refactor: remove unnecessary logging in error handling
yamcodes Jun 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .trunk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*out
*logs
*actions
*notifications
*tools
plugins
user_trunk.yaml
user.yaml
tmp
2 changes: 2 additions & 0 deletions .trunk/configs/.markdownlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Prettier friendly markdownlint config (all formatting rules disabled)
extends: markdownlint/style/prettier
7 changes: 7 additions & 0 deletions .trunk/configs/.shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enable=all
source-path=SCRIPTDIR
disable=SC2154

# If you're having issues with shellcheck following source, disable the errors via:
# disable=SC1090
# disable=SC1091
7 changes: 7 additions & 0 deletions .trunk/configs/.yamllint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
rules:
quoted-strings:
required: only-when-needed
extra-allowed: ["{|}"]
key-duplicates: {}
octal-values:
forbid-implicit-octal: true
14 changes: 14 additions & 0 deletions .trunk/configs/svgo.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false, // https://github.com/svg/svgo/issues/1128
sortAttrs: true,
removeOffCanvasPaths: true,
},
},
},
],
};
39 changes: 39 additions & 0 deletions .trunk/trunk.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This file controls the behavior of Trunk: https://docs.trunk.io/cli
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
version: 0.1
cli:
version: 1.22.11
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
plugins:
sources:
- id: trunk
ref: v1.7.0
uri: https://github.com/trunk-io/plugins
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
runtimes:
enabled:
- go@1.21.0
- node@22.16.0
- python@3.10.8
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
lint:
enabled:
- actionlint@1.7.7
- biome@1.9.4
- checkov@3.2.437
- git-diff-check
- markdownlint@0.45.0
- oxipng@9.1.5
- prettier@3.5.3
- shellcheck@0.10.0
- shfmt@3.6.0
- svgo@3.3.2
- trufflehog@3.88.35
- yamllint@1.37.1
actions:
disabled:
- trunk-announce
- trunk-check-pre-push
- trunk-fmt-pre-commit
enabled:
- trunk-upgrade-available
190 changes: 190 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Architecture

## Overview

This service uses a modular, flat, feature-sliced, **Layered Architecture** inspired by the [NestJS philosophy](https://docs.nestjs.com/#philosophy).

We separate the system into 3 main layers:

1. **Controller** – Talks to the client
2. **Service** – Handles the business logic
3. **Repository** – Interacts with the database

Each domain feature (e.g. `articles`, `profiles`, `tags`) is isolated into a top-level module folder, containing the above layers, and also:

- **Mapper** - Transforms data between layers
- **Schema** - Defines database tables and relations

```mermaid
graph TD
subgraph Controller Layer
C1[articles.controller.ts]
C2[comments.controller.ts]
C3[tags.controller.ts]
end

subgraph Service Layer
S1[articles.service.ts]
S2[comments.service.ts]
S3[tags.service.ts]
end

subgraph Repository Layer
R1[articles.repository.ts]
R2[comments.repository.ts]
R3[tags.repository.ts]
end

subgraph Schema Layer
SC1[articles.schema.ts]
SC2[comments.schema.ts]
SC3[tags.schema.ts]
SC4[article-tags.schema.ts]
end

subgraph Mapper Layer
M1[articles.mapper.ts]
M2[comments.mapper.ts]
M3[tags.mapper.ts]
end

C1 --> S1 --> R1 --> SC1
C2 --> S2 --> R2 --> SC2
C3 --> S3 --> R3 --> SC3

S1 --> M1
S2 --> M2
S3 --> M3

SC1 --> SC4
SC3 --> SC4
```

## Layer Responsibilities

### 1. Controller Layer (Client-facing)

- Receives data from the client (DTO)
- Returns data to the client (DTO)
- Validates data types
- Calls the service layer
- Can shape requests and responses, without performing any business logic

### 2. Service Layer (Business Logic)

- Contains the business logic
- Can perform any kind of calculation or transformation as long as it's part of the business rules
- Validates logic rules (e.g., checking if a user can register)
- Handles errors and logging
- Calls the repository layer to get or save data
- Can receive controller-level DTOs, but must map or validate them before passing data to the repository

### 3. Repository Layer (Database Access)

- Talks to the database
- Only responsible for saving and retrieving data
- **No** assumptions about validation
- **No** business logic should go here
- Handles pagination, sorting, and other database-specific operations
- Returns raw database rows, not domain entities

### Additional Layers

#### Mapper (Data Transformation)

- Transforms Row types from the database to domain entities or DTOs
- Performs camelCase vs. snake_case mapping if needed
- Convers Date to ISO strings for output, etc.

#### Schema (Database Definitions)

- Defines schemas using an ORM (e.g. `pgTable()` with Drizzle ORM and PostgreSQL)
- Optionally defines table relations (e.g. `relations()` with Drizzle ORM)

## Type Conventions

| Type | Layer | Purpose |
| ------------------------------------------------------ | ---------- | ---------------------------------------------- |
| `CreateThingDto`, `UpdateThingDto`, `ThingResponseDto` | Controller | Used to talk with the client |
| `IThing` | All | Common contract shared between layers |
| `Thing` | Repository | Defines how the data is stored in the database |

## General Design Principles

### 1. Flat, feature-sliced folder layout

- Each feature (e.g. `articles/`, `comments/`) contains all its layers in one folder
- No deep nesting, no shared `controllers/`, `services/` folders

### 2. One thing per file

- DTOs are defined in `dto/` folder, one file per DTO
- Domain entities are interfaces in `interfaces/`, one per file
- Row types are colocated in `interfaces/` and inferred from Drizzle schema

### 3. Relation-aware schema layer

Table relations are colocated with their schema definition unless they grow large.

### 4. Public API is shaped at the controller level

DTOs match the RealWorld spec (e.g., `{ article: ... }`) but this wrapping is handled in the controller, not baked into types.

## Type Design Principles

1. **Interfaces vs Classes**:

- Use interfaces (`IUser`) to define contracts between layers
- Use classes (`User`) for concrete implementations. The (database) entity is a concrete implementation of the interface.
- This separation allows for better testing and flexibility

2. **Canonical Forms**:

- Store canonical forms in the database (e.g., `birthdate`)
- The canonical form is represented in the entity (`User`) _and_ the interface (`IUser`)
- The DTO might use a different form, e.g. `CreateUserDto` might use `age` instead of `birthdate`
- Use mappers to convert between forms

3. **System vs Domain Properties**:
- System properties (`id`, `createdAt`, `updatedAt`) are managed by the base entity
- Domain properties (e.g. `email`, `name`) are defined in the interface, enforced by the entity, and controlled by the DTOs

## Examples

### Example 1: Can register?

```typescript
canRegister(user: Partial<IUser>) {
if (user.email.endsWith('@banned.com')) {
throw new ForbiddenException('Email domain is not allowed');
}

if (!this.isOldEnough(user.birthdate)) {
throw new ForbiddenException('User must be at least 13 years old');
}
}
```

This check lives in the service layer because:

- It's business logic
- It could change based on product decisions
- It might be reused across different controllers (`signup`, `adminCreateUser`, etc.)
- If tomorrow we add GraphQL on top of our REST, this logic will remain the same

### Example 2: Normalize email

```typescript
normalizeEmail(email: string) {
return email.toLowerCase().trim();
}
```

Also clearly service-level: it's a standardized rule, not controller-specific logic.

## See also

- More on **Project structure** - see [Project Structure](PROJECT_STRUCTURE.md)
- **Contributing** - see [Developer's Guide](CONTRIBUTING.md)
- **API Documentation** - see [RealWorld Backend Specifications](https://realworld-docs.netlify.app/specifications/backend/introduction/)
- **Drizzle ORM Documentation** - see [Drizzle ORM](https://orm.drizzle.team/)
91 changes: 91 additions & 0 deletions PROJECT_STRUCTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Project Structure

This document describes the file structure and organization of the codebase. The layout is flat and feature-sliced, with each domain feature (e.g., articles, comments) self-contained.

We favor **separation of concerns** over minimalism, and follow a **one file per thing** rule.

This layout is built to scale while staying clear, testable, and aligned with RealWorld API expectations.

## Top-Level Overview

```
src/
├── app.ts # Initializes and mounts the app
├── routes/ # Aggregates and mounts feature routers
├── db/ # Drizzle ORM config and database init
├── shared/ # Common utilities and helpers
├── articles/ # Full article feature module
├── comments/ # Full comment feature module
├── tags/ # Tag-related logic and schema
├── users/ # User logic (repo, profile dto)
```

## Feature Folder Layout

Each feature (e.g. `articles/`, `comments/`) uses this layout:

```
feature/
├── feature.controller.ts # REST handler logic
├── feature.service.ts # Business logic
├── feature.repository.ts # DB access logic
├── feature.mapper.ts # Converts DB to DTO
├── schema/
│ └── feature.schema.ts # Drizzle schema + optional relations
├── dto/
│ ├── create-feature.dto.ts # Input shape (TypeBox)
│ ├── update-feature.dto.ts # Input shape (if needed)
│ └── feature.dto.ts # Output DTO for response
├── interfaces/
│ ├── feature.interface.ts # Domain model
│ └── feature-row.interface.ts# Drizzle-inferred DB shape
```

## Folder-Level Purpose

### `/db/`

* Drizzle config and init
* Exports the db instance
* Does **not** export db tables, these are found as schemas inside feature folders

### `/shared/`

Global utilities, middleware, and shared concerns.

The following is a suggestion of what files might be found here:

```
shared/
├── auth-middleware.ts # Extracts auth context
├── http-errors.ts # Shared error classes
├── slugify.ts # Utility for slug generation
```

## DTO Naming

* `CreateThingDto` – used for `POST` requests
* `UpdateThingDto` – used for `PATCH`/`PUT` requests
* `ThingDto` – response structure
* Each DTO is defined via TypeBox and typed via `Static<typeof schema>`
* DTO files live in `dto/` and share name with their schema

### Interface Naming

* `Thing` (or `ThingEntity`) – the core domain interface used in services
* `ThingRow` – the shape returned by Drizzle ORM (via InferSelectModel)
* Stored in `interfaces/` and always one interface per file

### Schema Naming

* `feature.schema.ts` – contains Drizzle `pgTable()` definitions
* May also define `relations()` in the same file unless very large
* If split, name the second file `feature-relations.schema.ts`

## See also

- More on **Architecture** - see [Architecture](ARCHITECTURE.md)
- **Contributing** - see [Developer's Guide](CONTRIBUTING.md)
- **API Documentation** - see [RealWorld Backend Specifications](https://realworld-docs.netlify.app/specifications/backend/introduction/)
- **Drizzle ORM Documentation** - see [Drizzle ORM](https://orm.drizzle.team/)

3 changes: 3 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"useIgnoreFile": true,
"clientKind": "git"
},
"files": {
"ignore": [".trunk/**"]
},
"organizeImports": {
"enabled": true
},
Expand Down
Binary file modified bun.lockb
Binary file not shown.
6 changes: 5 additions & 1 deletion db/drop.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { exit } from 'node:process';
import {
articles,
comments,
favoriteArticles,
} from '@/articles/schema/favorite-articles.schema';
import { db } from '@/database.providers';
import { articles, comments, favoriteArticles } from '@articles/articles.model';
import dbConfig from '@db/config';
import { articleTags, tags } from '@tags/tags.model';
import { userFollows, users } from '@users/users.model';
Expand Down
Loading
Loading