@kithinji/orca
v1.0.19
Published
A fullstack web framework for Node.js and browser environments
Maintainers
Readme
The Problem
Modern web apps require two separate projects: one for the frontend, one for the backend. This means:
- Two repositories to maintain
- Two deployment pipelines
- Two sets of dependencies
- Manual API contracts that constantly drift
- Endless context switching between different codebases
Building a single feature requires touching multiple repos, keeping types in sync manually, and spending more time wiring things together than actually building.
Orca solves this by unifying your entire stack in one codebase.
What is Orca?
Orca is a full-stack TypeScript framework that lets you build your entire application (API, business logic, and UI) in a single codebase.
Write your backend services, frontend components, and API endpoints together with shared types and zero boilerplate.
Inspired By
- NestJS for backend architecture (dependency injection, decorators, modules)
- Angular for frontend structure (class-based components, clear organization)
- Islands Architecture for optimal performance (server-render by default, hydrate selectively)
How It Works
Orca uses three simple directives to control where code runs:
1. Default: Server Components
Components render on the server by default. Fast initial loads, works without JavaScript.
@Component()
export class ProductList {
constructor(private products: ProductService) {}
async build() {
const items = await this.products.getAll();
return (
<div>
{items.map(item => (
<ProductCard key={item.id} product={item} />
))}
</div>
);
}
}2. "use interactive" for Client Components
Add this directive when you need client-side interactivity like click handlers, forms, or animations.
"use interactive";
import { Component, signal } from "@kithinji/orca";
@Component()
export class AddToCartButton {
constructor(private cart: CartService) {}
props!: { productId: number };
private adding = signal(false);
build() {
return (
<button
disabled={this.adding.value}
onClick={async () => {
this.adding.value = true;
await this.cart.addItem(this.props.productId);
this.adding.value = false;
}}
>
{this.adding.value ? "Adding..." : "Add to Cart"}
</button>
);
}
}3. "use public" for Auto-Generated APIs
Mark services with this directive to automatically create type-safe API endpoints.
"use public";
import { Injectable } from "@kithinji/orca";
@Injectable()
export class CartService {
// This becomes: POST /cart/addItem
public async addItem(productId: number) {
const item = await this.db.products.findUnique({ where: { productId } });
return this.db.cart.create({ data: { productId, quantity: 1 } });
}
// This becomes: GET /cart/getTotal
public async getTotal() {
return this.db.cart.aggregate({ _sum: { price: true } });
}
}When you call this.cart.addItem() from a client component, Orca automatically:
- Generates the
/cart/addItemendpoint - Serializes your function call into a fetch request
- Validates input on the server
- Executes your server-side logic
- Returns the typed response
No manual API calls. No type drift. Just call methods like they're local.
Why Use Orca?
Type Safety Across Your Entire Stack
Your types never drift because they're literally the same types. Change a service method's signature and TypeScript catches every call site instantly, whether it's in a server component, client component, or controller.
Less Boilerplate, More Features
// Traditional approach: Define API route, write fetch call, handle errors
const response = await fetch('/api/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId })
});
const data = await response.json();
// Orca: Just call the method
await this.cart.addItem(productId);Islands Architecture for Performance
Server-render everything by default for fast initial loads. Add "use interactive" only to components that need client-side JavaScript. Your page stays lightweight while still providing rich interactivity where needed.
Clear Architecture That Scales
Dependency injection, decorators, and modules keep your code organized as your application grows. No more tangled mess of utility functions and scattered business logic.
Quick Start
Installation
Install the Orca CLI globally:
npm install -g @kithinji/podCreate a New Project
pod new my-app
cd my-app
npm run devOpen your browser to http://localhost:8080 to see your app.
Requirements
- Node.js v20.9 or higher
- A text editor (VS Code recommended)
Project Structure
src/
├── features/
│ ├── user/
│ │ ├── user.service.ts # Business logic
│ │ ├── user.controller.ts # HTTP endpoints (optional)
│ │ ├── pages/
│ │ │ ├── user-profile.page.tsx
│ │ │ └── user-login.page.tsx
│ │ ├── components/
│ │ │ └── avatar.component.tsx
│ │ └── user.module.ts # Feature module
│ └── product/
│ ├── product.service.ts
│ ├── components/
│ └── product.module.ts
└── app.module.ts # Root moduleOrganize by feature, not by technical layer. Everything related to "products" lives in the products folder.
Core Concepts
Services (Business Logic Layer)
Services contain your application logic. They're injectable classes that can be used anywhere.
"use public";
import { Injectable, Signature, Observable } from "@kithinji/orca";
import { z } from "zod";
const GetProductSchema = z.object({ id: z.number() });
const ProductSchema = z.object({
id: z.number(),
name: z.string(),
price: z.number(),
});
@Injectable()
export class ProductService {
constructor(private db: DatabaseService) {}
@Signature(GetProductSchema, ProductSchema)
public async getProduct(id: number) {
return this.db.products.findUnique({ where: { id } });
}
public async listProducts() {
return this.db.products.findMany();
}
// Type annotate with Observable for Server-Sent Events
public stream(): Observable<number> {
// Implementation
}
}The @Signature decorator defines validation schemas. Orca uses these to automatically validate requests.
Components (UI Layer)
Components are classes that return JSX from a build() method.
Server Component:
@Component()
export class ProductList {
constructor(private products: ProductService) {}
async build() {
const items = await this.products.listProducts();
return (
<div>
<h1>Products</h1>
{items.map(item => (
<div key={item.id}>
<h3>{item.name}</h3>
<p>${item.price}</p>
<AddToCartButton productId={item.id} />
</div>
))}
</div>
);
}
}Interactive Component:
"use interactive";
import { Component, signal, toSignal } from "@kithinji/orca";
@Component()
export class AddToCartButton {
constructor(
private cart: CartService,
private products: ProductService
) {}
props!: { productId: number };
private adding = signal(false);
build() {
// Streams become EventSource for Server-Sent Events
const stream = this.products.stream();
const value = toSignal(stream, this);
return (
<button
onClick={async () => {
this.adding.value = true;
await this.cart.addItem(this.props.productId);
this.adding.value = false;
}}
>
{this.adding.value ? "Adding..." : "Add to Cart"}
</button>
);
}
}Controllers (HTTP Endpoints)
Controllers define REST endpoints. You can write them manually or let Orca auto-generate them from "use public" services.
Manual Controller:
import { Controller, Get, Post, Body, Param } from "@kithinji/orca";
@Controller("/products")
export class ProductController {
constructor(private products: ProductService) {}
@Get()
async findAll() {
return this.products.listProducts();
}
@Get("/:id")
async findOne(@Param("id") id: number) {
return this.products.getProduct(id);
}
@Post()
async create(@Body() data: CreateProductDto) {
return this.products.create(data);
}
}Auto-Generated:
When you mark a service with "use public", Orca creates endpoints automatically:
POST /product/getProductGET /product/listProducts
No controller code required.
Modules (Organizing Features)
Modules group related functionality and manage dependencies.
import { Module } from "@kithinji/orca";
import { ProductController } from "./product.controller";
import { ProductService } from "./product.service";
import { ProductList, AddToCartButton } from "./components";
@Module({
imports: [DatabaseModule], // Dependencies
controllers: [ProductController], // HTTP endpoints
providers: [ProductService], // Services
declarations: [ProductList, AddToCartButton], // UI components
exports: [ProductService, ProductList] // What other modules can use
})
export class ProductModule {}Navigation (Stack-Based Routing)
Instead of file-based routing, push components onto a navigation stack.
import { Navigate, Component } from "@kithinji/orca";
@Component({
route: "/home/:id?location&browser*",
})
export class HomePage {
props!: {
id: string;
location: string;
browser?: string;
};
constructor(private navigate: Navigate) {}
build() {
return (
<div>
<h1>Welcome</h1>
<button onClick={() => this.navigate.push(<UserProfile userId={1} />)}>
View Profile
</button>
</div>
);
}
}Navigation Methods:
push()- Navigate to a new pagepop()- Go back to previous pagereplace()- Replace current pagepopToRoot()- Clear stack and return to rootcanPop()- Check if back navigation is possible
Using <a> tags:
You can still use anchor tags, but they're not type-safe:
<a href="/home/123?location=nairobi&browser=FireFox">To Home</a>Complete Example
Here's how everything works together:
// cart.service.ts
"use public";
import { Injectable } from "@kithinji/orca";
@Injectable()
export class CartService {
constructor(private db: DatabaseService) {}
public async addItem(productId: number, quantity: number = 1) {
const product = await this.db.products.findUnique({ where: { productId } });
return this.db.cart.create({
data: { productId, quantity, price: product.price }
});
}
public async getCart() {
return this.db.cart.findMany();
}
}// cart-page.component.tsx
import { Component } from "@kithinji/orca";
@Component()
export class CartPage {
constructor(private cart: CartService) {}
async build() {
const items = await this.cart.getCart();
return (
<div>
<h1>Your Cart</h1>
{items.map(item => (
<CartItem key={item.id} item={item} />
))}
</div>
);
}
}// add-to-cart-button.component.tsx
"use interactive";
import { Component, Navigate } from "@kithinji/orca";
@Component()
export class AddToCartButton {
constructor(
private cart: CartService,
private navigate: Navigate
) {}
props!: { productId: number };
build() {
return (
<button
onClick={async () => {
await this.cart.addItem(this.props.productId);
this.navigate.push(<CartPage />);
}}
>
Add to Cart
</button>
);
}
}What happens:
CartServicehas"use public", so Orca generates API endpointsCartPage(server component) callscart.getCart()on the server during renderAddToCartButton(interactive component) callscart.addItem()from the browser- Orca converts that call into
fetch('/cart/addItem', ...) - Types are preserved everywhere. TypeScript catches errors before runtime
- Navigation happens by pushing components, not URL strings
When to Use Orca
Orca is great for:
- Solo developers or small teams
- Building features quickly without managing two repos
- Internal tools, dashboards, or admin panels
- Highly interactive web applications
- Projects where you need a real API (for mobile apps, CLIs, webhooks)
- Teams tired of keeping types in sync between frontend and backend
Orca might not fit if:
- You have separate frontend and backend teams
- You prefer the traditional split and it works for you
- You need a purely client-side SPA
- You're building a static marketing site or blog (use Astro or Next.js)
- Your team is heavily invested in another stack
Orca vs. Alternatives
Why not separate repos?
Separate repos mean double the maintenance. Two sets of types that drift. Two deployment pipelines. Constant context switching. For solo devs or small teams, you spend more time connecting things than building.
Why not Next.js?
Next.js excels at rendering UI fast, but it's optimized for frontend-first development. The moment you need a proper backend with controllers, services, and dependency injection, things get messy. Server Actions blur the lines between UI and backend in ways that make larger apps hard to maintain.
Orca gives you a real backend architecture with clean separation of concerns, plus UI rendering that integrates naturally.
Why not HTMX?
HTMX is elegant for server-rendered apps. But endpoints that return HTML lock you in. When you need a mobile app, CLI, or webhook consumer, you either build a second JSON API or parse HTML on the client.
Orca's API returns JSON by default. UI rendering is a layer on top, not a replacement. You're never painted into a corner.
Philosophy
Orca rejects the artificial divide between frontend and backend. For large teams with dedicated specialists, that split makes sense. But for solo developers and small teams, it doubles the work.
Core Principles
One Codebase - Write features end to end in one place
Islands of Interactivity - Server-render by default, add JavaScript only where needed
Type-Safe APIs - No manual fetch calls, no drifting types
Clear Architecture - Dependency injection, decorators, and modules keep large apps maintainable
Stack-Based Navigation - Route by pushing components, not folder structures
This is opinionated software. It has rules, structure, and conventions. The rules exist to free you from decision fatigue so you can focus on building.
Documentation
Full guides, API references, and examples at orca.dafifi.net
Topics covered:
- Components and JSX
- Dependency Injection
- Modules and Providers
- Controllers and Routing
- The
"use interactive"directive - The
"use public"directive - Signals and Reactivity
- Observables and Server-Sent Events
- Navigation Stack
- Validation with Zod
Roadmap
- [ ] Database integrations (TypeORM, Prisma)
- [ ] Authentication and authorization modules
- [ ] WebSocket support
- [ ] GraphQL adapter
- [ ] CLI scaffolding improvements
- [ ] File upload handling
- [ ] Background jobs and task queues
Contributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
Stay Connected
- Author: Kithinji Brian
- Website: orca.dafifi.net
- NPM: @kithinji/orca
License
MIT
