|
1 | 1 | # SojuStack |
2 | 2 |
|
3 | | -My constantly updated opinionated stack for constructing new Fullstack(Web/Mobile/Backend) applications. |
| 3 | +Opinionated, production-minded full-stack starter with strong TypeScript ergonomics and end-to-end type safety. |
4 | 4 |
|
5 | | -## Getting Started |
| 5 | +## Tech Stack |
6 | 6 |
|
7 | | -I've provided both automated and manual setup options to fit your workflow. |
| 7 | +### Monorepo and Tooling |
8 | 8 |
|
9 | | -### 🚀 Quick Start (Automated) |
| 9 | +- `pnpm` workspaces |
| 10 | +- `turbo` task orchestration |
| 11 | +- `TypeScript` everywhere |
| 12 | +- `oxlint` + `oxfmt` |
| 13 | +- `lefthook` (pre-push checks) |
10 | 14 |
|
11 | | -Only use quick-start for trying the stack out initially. Don't use this during actual development. |
| 15 | +### Web (`apps/web`) |
12 | 16 |
|
13 | | -```sh |
14 | | -$ pnpm dev:setup |
| 17 | +- `TanStack Start` + `TanStack Router` |
| 18 | +- `TanStack Query` |
| 19 | +- `openapi-fetch` generated client types |
| 20 | +- `Tailwind CSS` + `shadcn` + `@base-ui/react` |
| 21 | +- `better-auth` client integration |
| 22 | + |
| 23 | +### API (`apps/api`) |
| 24 | + |
| 25 | +- `NestJS` (v11) |
| 26 | +- `Drizzle ORM` + `drizzle-kit` |
| 27 | +- `PostgreSQL` |
| 28 | +- `Valkey`/Redis-compatible caching via `Keyv` |
| 29 | +- `Better Auth` |
| 30 | +- OpenAPI generation (`@nestjs/swagger` + `openapi-typescript`) |
| 31 | + |
| 32 | +### Local Infra (`docker-compose.yaml`) |
| 33 | + |
| 34 | +- `Postgres` |
| 35 | +- `Valkey` |
| 36 | +- `Mailpit` |
| 37 | +- `RustFS` (S3-compatible object storage) |
| 38 | + |
| 39 | +## Architecture |
| 40 | + |
| 41 | +```mermaid |
| 42 | +flowchart LR |
| 43 | + U[User] --> W[Web App<br/>TanStack Start] |
| 44 | + W -->|HTTP + Cookies| A[API<br/>NestJS] |
| 45 | + A --> P[(PostgreSQL)] |
| 46 | + A --> V[(Valkey)] |
| 47 | + A --> S[(RustFS / S3-compatible)] |
| 48 | + A --> M[Mailpit] |
15 | 49 | ``` |
16 | 50 |
|
17 | | -### 🛠️ Manual Setup (Recommended) |
| 51 | +## Type-Safe Flow |
18 | 52 |
|
19 | | -For more control over the setup process, run the following in the order listed at the root of the project. |
| 53 | +```mermaid |
| 54 | +sequenceDiagram |
| 55 | + participant FE as Web |
| 56 | + participant API as NestJS API |
| 57 | + participant OAS as OpenAPI Types |
20 | 58 |
|
21 | | -```sh |
22 | | -# 1. Create environment files |
23 | | -$ cp apps/api/.env.example apps/api/.env |
24 | | -$ cp apps/web/.env.example apps/web/.env |
| 59 | + FE->>API: Request typed by openapi-fetch client |
| 60 | + API-->>FE: Response typed by generated schema |
| 61 | + API->>OAS: Generate/update openapi.d.ts |
| 62 | + OAS-->>FE: Compile-time contract updates |
| 63 | +``` |
25 | 64 |
|
26 | | -# 2. Install dependencies |
27 | | -$ pnpm install |
| 65 | +## Getting Started |
28 | 66 |
|
29 | | -# 3. Start Docker services |
30 | | -$ docker-compose up -d |
| 67 | +### Quick Trial (Automated) |
31 | 68 |
|
32 | | -# 4. Setup database |
33 | | -$ pnpm -C ./apps/api db:push |
| 69 | +Use this only for first-time trial runs: |
34 | 70 |
|
35 | | -# 5. Start development servers |
36 | | -$ pnpm dev |
| 71 | +```sh |
| 72 | +pnpm dev:setup |
37 | 73 | ``` |
38 | 74 |
|
39 | | -#### Default Service URLs |
| 75 | +### Manual Setup (Recommended) |
| 76 | + |
| 77 | +```sh |
| 78 | +# 1) Create env files |
| 79 | +cp apps/api/.env.example apps/api/.env |
| 80 | +cp apps/web/.env.example apps/web/.env |
| 81 | + |
| 82 | +# 2) Install dependencies |
| 83 | +pnpm install |
| 84 | + |
| 85 | +# 3) Start local infrastructure |
| 86 | +docker-compose up -d |
| 87 | + |
| 88 | +# 4) Apply DB schema |
| 89 | +pnpm -C apps/api db:push |
40 | 90 |
|
| 91 | +# 5) Start apps |
| 92 | +pnpm dev |
41 | 93 | ``` |
42 | | -web: http://localhost:3000 |
43 | | -api: http://localhost:8080 |
44 | | -mailpit: http://localhost:8025 |
| 94 | + |
| 95 | +## Default Local Endpoints |
| 96 | + |
| 97 | +```txt |
| 98 | +web: http://localhost:3000 |
| 99 | +api: http://localhost:8080 |
| 100 | +mailpit: http://localhost:8025 |
45 | 101 | postgres: postgres://postgres:postgres@localhost:5432/postgres |
46 | | -redis: redis://localhost:6379 |
47 | | -minio: http://localhost:9000 |
| 102 | +valkey: redis://localhost:6379 |
| 103 | +rustfs: http://localhost:9000 |
48 | 104 | ``` |
49 | 105 |
|
50 | | -## E2E Typesafety |
| 106 | +## Common Commands (Root) |
51 | 107 |
|
52 | | -When developing applications with a smaller team(or solo) that tends to pivot, E2E typesafety is pretty imparative to develop with confidence. This stack acheives E2E type-safety through a few different means, |
| 108 | +```sh |
| 109 | +pnpm dev # run web + api dev tasks via turbo |
| 110 | +pnpm build # build all packages |
| 111 | +pnpm lint # oxlint type-aware run |
| 112 | +pnpm lint:fix # apply lint fixes |
| 113 | +pnpm typecheck # turbo typecheck |
| 114 | +pnpm format # oxfmt |
| 115 | +pnpm api:openapi:generate |
| 116 | +``` |
53 | 117 |
|
54 | | -1. A monorepo where types can be shared. |
55 | | -2. OpenAPI(Swagger) docs actively generated. |
56 | | -3. OpenAPI-Fetch client on the frontends. |
| 118 | +## End-to-End Type Safety |
57 | 119 |
|
58 | | -**There is no need to regnerate types in the SojuStack, this is done automatically.** The only necessity is to ensure DTO's have their correct OpenAPI annotations. |
| 120 | +SojuStack keeps API and frontend contracts aligned by default: |
59 | 121 |
|
60 | | -This is achieved by a non-blocking helper function that listens and regenerates the `openapi.d.ts` file upon any changes - instantly reflecting on all frontend clients. Go ahead, change an endpoint or DTO in NestJS and view the errors in the frontend clients. |
| 122 | +- Shared monorepo context |
| 123 | +- OpenAPI docs and generated type definitions |
| 124 | +- Typed API usage through `openapi-fetch` |
61 | 125 |
|
62 | | -## Whats Inside and Why? |
| 126 | +In practice: update a DTO/route in API and frontend compile errors surface immediately where contracts changed. |
63 | 127 |
|
64 | | -Hopefully this helps understand my logic and reasoning behind my choices in technology. |
| 128 | +## Why This Stack |
65 | 129 |
|
66 | 130 | ### Axioms |
67 | 131 |
|
68 | 132 | 1. Does it solve the problem? |
69 | | -2. Is it tested, stable, well documented, and battle-proven? |
70 | | -3. No lock-in or ability to migrate away from the choice at anytime. |
71 | | -4. Will it scale with my needs? |
72 | | -5. Is the DX well thought out? |
| 133 | +2. Is it battle-tested and documented? |
| 134 | +3. Can I migrate away without pain? |
| 135 | +4. Does it scale with me? |
| 136 | +5. Is the developer experience actually good? |
| 137 | +6. Does it work well with AI(yeah, we're here so might as well) |
73 | 138 |
|
74 | 139 | ### Backend |
75 | 140 |
|
76 | | -#### NestJS (Framework) |
| 141 | +#### NestJS |
77 | 142 |
|
78 | | -NestJS is the first to check all of the boxes above. It's on its v11 release, its proven itself for over a decade, it's incredibly easy to scale with many options available, and its documentation is fantastic. |
| 143 | +It checks all the boxes for me: mature, structured, testable, and easy to grow. |
| 144 | +People call it verbose; I call it explicit. I prefer that when business logic gets real. |
79 | 145 |
|
80 | | -It gets a bad rap in the node world for being verbose or from those who hate "OOP" in a "functional language". I for one embrace OOP in JS as it naturally documents your applications domain and business logic, keeps your application easily testable, and idk...its the way I mentally model things already so it translates well in my case. Also, have you ever written a monad? |
| 146 | +#### PostgreSQL |
81 | 147 |
|
82 | | -Testability is also a massive incentive for building in NestJS. I'm not the largest fan of TDD or unit testing, but in some cases it sure does come in handy. And when you finally have the needs to write tests, you'll be thankful for an environment that makes it painless. |
| 148 | +I'm a relational DB person. Postgres has the right defaults and enough headroom that I rarely regret choosing it. |
83 | 149 |
|
84 | | -#### Postgres (Database) |
| 150 | +#### Drizzle |
85 | 151 |
|
86 | | -I'm a relational database type of guy. NoSQL databases have their role and place in the world but by large and whole you can't go wrong with Postgres. The only other real option is MySQL, which has some benefits, but in my experience I always end up wanting to reach for a feature or plugin it lacks. |
| 152 | +This is the SQL-first ORM experience I actually enjoy. |
| 153 | +Minimal magic, typed queries, reviewable migrations, and no giant abstraction tax. |
87 | 154 |
|
88 | | -#### Drizzle (ORM) |
| 155 | +#### Valkey (Redis-compatible cache) |
89 | 156 |
|
90 | | -Drizzle is early, sure - but its also very, very, very simple. Its a query builder with migrations. I've had more confidence using Drizzle than the industry standard TypeORM by a long shot. |
91 | | -If you know SQL, you know Drizzle and I like it for a few reasons, |
| 157 | +Simple, fast, and drop-in for Redis workflows. |
| 158 | +With `Keyv` in the API layer, swapping providers is mostly config-level work. |
92 | 159 |
|
93 | | -1. Its basic and simple; there is no magic. You write SQL like typescript and it generates pure SQL migrations to review. Infinite room to grow and no regrets to be had(ALWAYS REVIEW YOUR SQL MIGRATIONS MANUALLY, THIS IS TRUE FOR ANYTHING). |
94 | | -2. It offers a type-safe, SQL-first approach that strikes an ideal balance between raw SQL flexibility and modern TypeScript ergonomics. |
95 | | -3. It doesn't need an ecosystem like traditional ORMS. Its built to write sql and typesafe sql you shall write. |
| 160 | +#### Better Auth |
96 | 161 |
|
97 | | -#### Redis (Cache) |
| 162 | +"Don't roll your own crypto" is still the rule. |
| 163 | +Better Auth gives enough abstraction to move fast without boxing me in. |
98 | 164 |
|
99 | | -Substitute this for ValKey if you wish. The cache manager in NestJS uses KeyV as an interface so the only migration needed is to change the URL. |
| 165 | +#### RustFS (S3-compatible object storage) |
100 | 166 |
|
101 | | -#### Better-Auth (Auth) |
| 167 | +Local-first, self-hostable, and S3-compatible. |
| 168 | +If I move to managed object storage later, it's mostly endpoint/credential changes. |
102 | 169 |
|
103 | | -I'm a fan of "roll your own auth, not your own encryption". Its really not that hard to setup, but after evaluating Better-Auth I feel comfortable enough to include it as my auth solution. It does what I need it to do, provides an adequate level of abstraction, then gets out of the way. I've tested implementing better-auth then migrating away to a "self-rolled" auth and its **incredidbly** simple which gives me more confidence to build with it. |
| 170 | +### Frontend |
104 | 171 |
|
105 | | -#### MinIO (Storage) |
| 172 | +#### TanStack Start + Router |
106 | 173 |
|
107 | | -Minio is just self-hosted open source storage. Its fully S3 compatible so if you ever wanna switch over, its just a url change. The reason im a fan of MinIO is it keeps me out of the AWS console, lets me emulate my entire stack locally, and I can throw it on any VPS right next to my other services. Throw a CDN infront of it and it's quicker than S3. |
| 174 | +Fantastic routing model and good long-term ergonomics. |
| 175 | +File-based routes + typed APIs make iteration fast and refactors safer. |
108 | 176 |
|
109 | | -### Frontend |
| 177 | +#### TanStack Query |
| 178 | + |
| 179 | +The default for server state in React apps for a reason. |
| 180 | + |
| 181 | +#### shadcn + Base UI + Tailwind |
| 182 | + |
| 183 | +Composable primitives, easy customization, and no hard lock-in to a black-box UI kit. |
110 | 184 |
|
111 | | -#### Tanstack Start (Framework) |
| 185 | +### Monorepo + Tooling |
112 | 186 |
|
113 | | -I was hesitant to build ontop of this as its pre v1.0 release, but after seeing how far its come in such a short amount of time, the "breaking changes" guide being so incredibly well updated and documented, and its DX - its too good to let go. |
| 187 | +#### pnpm + Turbo |
114 | 188 |
|
115 | | -This is the ONLY pre-release tech i'm willing to bet on for production at this time. I left for Svelte after seeing the direction of React and the horrific war-crimes Vercel/NextJS committed. TanStack Start brought me back to React. |
| 189 | +Fast workspace installs, clean task orchestration, and predictable monorepo workflows. |
116 | 190 |
|
117 | | -#### Tanstack Query (Data Fetching) |
| 191 | +#### Oxlint + Oxfmt |
118 | 192 |
|
119 | | -IDK what to say on this one, its obvious. If you remember what life was like before Tanstack Query having to fetch data with redux/thunks - you'd be a donator to the project like I am. |
| 193 | +Very fast lint/format feedback loops with type-aware checks where it matters. |
120 | 194 |
|
121 | | -#### ShadCN (UI) |
| 195 | +#### Lefthook |
122 | 196 |
|
123 | | -I have no preference in UI components. Use what you like and there's not really a wrong answer - this is purely what you enjoy and don't let anyone tell you differently. I use ShadCN for the same reasons i mentioned about every other tech above. It does what I need, gets out of my way when I want it to, and I can easily migrate away from it without fear of lock-in. |
| 197 | +Simple guardrails before push (`lint` + `typecheck`) so broken code is harder to ship. |
0 commit comments