Spoosh
Core

Schema Definition

Define type-safe API schemas with TypeScript

Spoosh uses TypeScript types to define your API structure. This gives you full autocomplete and type checking for all API calls.

Basic Schema

Define your API schema as a flat path-based type structure:

type User = {
  id: number;
  name: string;
  email: string;
};

type ApiSchema = {
  users: {
    GET: { data: User[] };
    POST: { data: User; body: { name: string; email: string } };
  };
  "users/:id": {
    GET: { data: User };
    PUT: { data: User; body: Partial<User> };
    DELETE: void;
  };
};

Endpoint Types

{ data }

For endpoints that only return data:

const {  } = await ("users").GET();

{ data; body }

Endpoint with a request body:

const {  } = await ("users").POST({
  : { : "John", : "john@example.com" },
});

{ data; query }

Endpoint with query parameters:

const {  } = await ("users").GET({
  : { : 1, : 10 },
});

{ data; body } with Files

Use the form() wrapper to send file uploads as multipart/form-data:

import {  } from "@spoosh/core";

const  = <>("/api");

const {  } = await ("users/avatar").POST({
  : ({ :  }),
});

Dynamic Path Segments

Use path parameters in your schema keys:

type ApiSchema = {
  users: {
    GET: { data: User[] };
  };
  "users/:id": {
    GET: { data: User };
    PUT: { data: User; body: Partial<User> };
    DELETE: void;
  };
};

// Usage
const { data: users } = await api("users").GET(); // GET /users
const { data: user } = await api("users/:id").GET({ params: { id: 123 } }); // GET /users/123
const { data } = await api("users/:id").PUT({ params: { id: 123 }, body: {} }); // PUT /users/123
await api("users/:id").DELETE({ params: { id: 123 } }); // DELETE /users/123

Nested Dynamic Segments

type ApiSchema = {
  "users/:userId/posts": {
    GET: { data: Post[] };
  };
  "users/:userId/posts/:postId": {
    GET: { data: Post };
  };
};

// Usage
await api("users/:userId/posts").GET({ params: { userId: 1 } }); // GET /users/1/posts
await api("users/:userId/posts/:postId").GET({
  params: { userId: 1, postId: 42 },
}); // GET /users/1/posts/42

With Angular Injection Functions

Dynamic params work seamlessly with injectRead and injectWrite:

import { Component, input, computed } from "@angular/core";
import { injectRead, injectWrite } from "./api/client";

@Component({
  selector: "app-user-posts-list",
  template: `
    <div>
      <button (click)="handleCreatePost()">Create Post</button>
      @for (post of posts(); track post.id) {
        <div>
          <h3>{{ post.title }}</h3>
          <button (click)="handleDeletePost(post.id)">Delete</button>
        </div>
      }
    </div>
  `,
})
export class UserPostsListComponent {
  userId = input.required<number>();

  private userPosts = injectRead((api) =>
    api("users/:userId/posts").GET({ params: { userId: this.userId() } })
  );
  posts = computed(() => this.userPosts.data());

  private createPost = injectWrite((api) => api("users/:userId/posts").POST());
  private deletePost = injectWrite((api) =>
    api("users/:userId/posts/:postId").DELETE()
  );

  async handleCreatePost() {
    await this.createPost.trigger({
      params: { userId: this.userId() },
      body: { title: "New Post" },
    });
  }

  async handleDeletePost(postId: number) {
    await this.deletePost.trigger({
      params: { userId: this.userId(), postId },
    });
  }
}

HTTP Methods

Spoosh supports all common HTTP methods:

MethodDescription
GETGET request
POSTPOST request
PUTPUT request
PATCHPATCH request
DELETEDELETE request

Schema Organization

For larger APIs, organize your schema into separate files:

src/api/schema/users.ts
export type UsersSchema = {
  users: {
    GET: { data: User[]; query: { page?: number } };
    POST: { data: User; body: CreateUserBody };
  };
  "users/:id": {
    GET: { data: User };
    PUT: { data: User; body: UpdateUserBody };
    DELETE: void;
  };
};
src/api/schema/posts.ts
export type PostsSchema = {
  posts: {
    GET: { data: Post[] };
    POST: { data: Post; body: CreatePostBody };
  };
};
src/api/schema/index.ts
import type { UsersSchema } from "./users";
import type { PostsSchema } from "./posts";

export type ApiSchema = UsersSchema & PostsSchema;

Type Inference from Server Frameworks

Hono

If you're using Hono on the server, you can automatically infer your schema:

import type { HonoToSpoosh } from "@spoosh/hono";
import type { hc } from "hono/client";
import type { AppType } from "./server";

type ApiSchema = HonoToSpoosh<ReturnType<typeof hc<AppType>>>;

See the Hono guide for details.

Elysia

If you're using Elysia on the server, you can automatically infer your schema:

import type { ElysiaToSpoosh } from "@spoosh/elysia";
import type { App } from "./server";

type ApiSchema = ElysiaToSpoosh<App>;

See the Elysia guide for details.

Summary

TypeDescriptionExample
{ data }Endpoint with data onlyGET: { data: User[] }
{ data; body }Endpoint with JSON bodyPOST: { data: User; body: CreateUserBody }
{ data; query }Endpoint with query paramsGET: { data: User[]; query: { page: number } }
{ data; error }Endpoint with typed errorGET: { data: User; error: ApiError }
voidNo response bodyDELETE: void
"path/:param"Dynamic path segment"users/:id": { GET: { data: User } }

On this page