Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
<img src="./assets/mascot-readme.png" alt="Tabi Mascot" width="200"/>
</p>

> Tabi (旅) means "journey" in Japanese. The shortest path between two points is
> a straight line. Tabi stays out of your way—no abstractions to fight, no magic
> to reverse-engineer, no surprises in production. Every build should be a
> stress free journey.

## Quick Start

```bash
Expand Down
48 changes: 48 additions & 0 deletions docs/adr/001-deno-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ADR-001: Use Deno as the Single Runtime

**Status:** Accepted

## Context

Modern web frameworks can target multiple JavaScript runtimes:

- **Node.js** - Mature ecosystem, widespread adoption, CommonJS/ESM complexity
- **Deno** - Native TypeScript, secure by default, modern standards
- **Bun** - Fast startup, Node.js compatible, newer/less stable
- **Runtime-agnostic** - Maximum compatibility, lowest common denominator
features

Each approach has trade-offs in ecosystem access, security model, developer
experience, and maintenance burden.

## Decision

Tabi targets Deno exclusively, requiring version 2.5.6 or higher.

We will not maintain Node.js compatibility layers or abstract runtime-specific
APIs behind adapters.

## Consequences

### Benefits

- Native TypeScript execution without build step or configuration
- Built-in security permissions model (network, filesystem, env access must be
explicit)
- Standard library (`@std/*`) for common utilities without third-party
dependencies
- Modern web standards (Fetch API, Web Crypto, Streams) as first-class citizens
- Single lockfile format and dependency resolution strategy
- Publish to JSR with native TypeScript support

### Drawbacks

- Smaller ecosystem than Node.js
- Some npm packages may not work without compatibility layer
- Users must install Deno (less ubiquitous than Node.js)
- Deployment targets limited to Deno-supporting platforms

### Neutral

- Version pinning via `.dvmrc` for team consistency
- Deno Deploy as natural deployment target (but not required)
76 changes: 76 additions & 0 deletions docs/adr/002-minimal-philosophy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ADR-002: Minimal Framework Philosophy

**Status:** Accepted

## Context

Web frameworks exist on a spectrum:

**Batteries-included** (Rails, Django, Nest.js):

- Prescriptive structure and conventions
- Built-in ORM, auth, sessions, templating
- Faster initial development, steeper learning curve
- Harder to deviate from conventions

**Minimal** (Express, Fastify, Hono):

- Core routing and middleware only
- Bring your own everything else
- More setup, more flexibility
- Explicit over implicit

Tabi needs to choose where on this spectrum to position itself.

## Decision

Tabi follows a minimal, explicit, no-magic philosophy.

Core principles:

- **No implicit behavior** - Every feature must be explicitly enabled
- **No hidden state** - Request/response flow is predictable and traceable
- **No framework lock-in** - Standard interfaces over proprietary abstractions
- **Composition over inheritance** - Build applications by composing small
pieces

The framework provides:

- Request/response handling
- Routing
- Middleware composition
- Core utilities (cookies, validation, compression)

The framework does not provide:

- Database/ORM layer
- Authentication/authorization
- Session management
- Templating engine
- Background jobs
- Email sending

## Consequences

### Benefits

- Smaller bundle size (only include what you use)
- Easier to understand (less magic to learn)
- Predictable behavior (explicit composition)
- Flexible architecture (no prescribed patterns)
- Easier to test (fewer hidden dependencies)
- Security benefits (smaller attack surface)

### Drawbacks

- More initial setup for common features
- Must choose and integrate third-party solutions
- Less guidance for application structure
- Potential for inconsistency across applications

### Implications

- Documentation must clearly show composition patterns
- Example applications should demonstrate common setups
- Middleware should be independently useful
- Each feature should have single, clear responsibility
75 changes: 75 additions & 0 deletions docs/adr/003-monorepo-composable-modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ADR-003: Monorepo with Independently Composable Modules

**Status:** Accepted

## Context

The framework consists of multiple features: core routing, validation, various
middleware (CORS, CSRF, compression, etc.), and utilities. These can be
organized as:

**Single monolithic package:**

- One import, one version
- All features always available
- Larger bundle if tree-shaking fails

**Separate packages with independent versions:**

- Import only what you need
- Version compatibility matrix complexity
- More release management overhead

**Single package with submodule exports:**

- One version to track
- Import specific modules
- Tree-shaking friendly
- Simpler release process

## Decision

Organize as a monorepo with a single JSR package (`@tabirun/app`) that exposes
submodule exports, each independently importable.

```typescript
// Import only what you need
import { TabiApp } from "@tabirun/app/app";
import { cors } from "@tabirun/app/cors";
import { rateLimit } from "@tabirun/app/rate-limit";
```

Each module:

- Has its own directory with `mod.ts` as public API
- Contains colocated tests
- Has independent README documentation
- Exports only public types and functions

## Consequences

### Benefits

- Single version number for entire framework
- Simpler dependency management for users
- Tree-shaking removes unused modules
- Easier cross-module testing
- Consistent release cycle
- No version compatibility matrix

### Drawbacks

- Breaking change in any module bumps entire package version
- Cannot patch individual modules independently
- All modules must maintain same quality bar

### Module Structure

```
module-name/
├── mod.ts # Public exports only
├── middleware.ts # Implementation
├── types.ts # Types (if needed)
├── README.md # Usage documentation
└── tests/ # Module tests
```
74 changes: 74 additions & 0 deletions docs/adr/004-standard-schema-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# ADR-004: Standard Schema for Validation Library Agnosticism

**Status:** Accepted

## Context

Runtime validation at API boundaries is essential for security and type safety.
Popular validation libraries include:

- **Zod** - TypeScript-first, excellent inference, large bundle
- **Valibot** - Smaller bundle, modular design
- **ArkType** - Fast runtime, complex syntax
- **Yup** - Mature, widespread adoption
- **Joi** - Battle-tested, verbose API

Each has different APIs, performance characteristics, and bundle sizes. Coupling
the framework to one library forces that choice on all users.

The `@standard-schema/spec` initiative defines a common interface that
validation libraries can implement, enabling interoperability.

## Decision

Use the Standard Schema interface (`@standard-schema/spec`) for all validation
integration. Provide Zod as the reference implementation in documentation and
examples.

```typescript
import type { StandardSchemaV1 } from "@standard-schema/spec";

interface ValidatorConfig {
json?: StandardSchemaV1;
query?: StandardSchemaV1;
params?: StandardSchemaV1;
form?: StandardSchemaV1;
}
```

Users can use any Standard Schema-compatible library:

```typescript
// With Zod
import { z } from "zod";
const schema = z.object({ name: z.string() });

// With Valibot
import * as v from "valibot";
const schema = v.object({ name: v.string() });

// Both work with Tabi's validator
const { validate, valid } = validator({ json: schema });
```

## Consequences

### Benefits

- No lock-in to specific validation library
- Users choose based on their preferences (bundle size, API, performance)
- Middleware remains library-agnostic
- Future libraries automatically compatible if they implement Standard Schema

### Drawbacks

- Slightly more complex type signatures
- Must document which libraries support Standard Schema
- Error message format varies by library
- Testing should cover multiple schema implementations

### Implementation Notes

- Validator middleware accepts any `StandardSchemaV1` compliant schema
- Type inference works through Standard Schema's `InferOutput` type
- Error reporting can be customized via `onError` option
80 changes: 80 additions & 0 deletions docs/adr/005-linear-router-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ADR-005: Linear Router as Default Implementation

**Status:** Accepted

## Context

Route matching algorithms have different trade-offs:

**Linear scan O(n):**

- Simple implementation
- Predictable matching order
- Performance degrades with route count
- Easy to debug

**Radix tree O(k) where k = path length:**

- Fast for large route sets
- Complex implementation
- Matching order less intuitive
- Harder to debug edge cases

**Compiled regex:**

- Very fast after compilation
- Complex to implement correctly
- Startup cost for compilation

For a minimal framework targeting small-to-medium applications, simplicity and
predictability may outweigh raw performance.

## Decision

Implement `LinearRouter` as the default router, with a pluggable `TabiRouter`
interface allowing alternative implementations.

```typescript
interface TabiRouter {
add(method: string, path: string, handler: TabiHandler): void;
match(method: string, path: string): RouteMatch | null;
}
```

Route matching priority:

1. Static routes (exact match)
2. Parameterized routes (`:id`)
3. Wildcard routes (`*`)

Within each category, first registered wins.

## Consequences

### Benefits

- Predictable matching (registration order within priority tiers)
- Simple debugging (linear search, clear priority rules)
- Easy to implement path traversal protection
- Sufficient performance for typical applications (<100 routes)
- Clear mental model for users

### Drawbacks

- O(n) matching per request
- Not suitable for applications with thousands of routes
- May need optimization for high-traffic scenarios

### Future Options

- `RadixRouter` can be added as alternative implementation
- Users can implement custom routers via `TabiRouter` interface
- Router choice doesn't affect application code (same API)

### Security

Linear router enables straightforward path traversal protection:

- Decode URL parameters
- Block `../`, `..\\`, and encoded variants
- Return 400 Bad Request on traversal attempts
Loading