Skip to content

Zero to a working route in under five minutes.

asciicast

Create & install

sh
npm create kosmo
cd my-app
npm install
sh
pnpm create kosmo
cd my-app
pnpm install
sh
yarn create kosmo
cd my-app
yarn install

Add a source folder

sh
npm run +folder
sh
pnpm +folder
sh
yarn +folder

You'll be prompted for a folder name, base URL, framework, and backend.

Framework and backend are both optional - each select offers a None choice:

  • for a backend-only service pick None as the framework.
  • for a client-only folder pick None as the backend.

In non-interactive mode there is no none value to pass - simply omit --framework or --backend and that side is skipped.

Next: Install newly added dependencies:

sh
npm install
sh
pnpm install
sh
yarn install

Start the dev server

sh
npm run dev
sh
pnpm dev
sh
yarn dev

Create a route

Create the file api/users/[id]/index.ts - KosmoJS detects it and generates starter code automatically.

Replace the generated content with something real:

ts
import { defineRoute } from "_/api";

export default defineRoute<"users/[id]">(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.req.param();
    return ctx.json({ id, name: "Jane Smith", email: "jane@example.com" });
  }),
]);
ts
import { defineRoute } from "_/api";

export default defineRoute<"users/[id]">(({ GET }) => [
  GET(async (event) => {
    const { id } = event.context.params;
    return { id, name: "Jane Smith", email: "jane@example.com" };
  }),
]);
ts
import { defineRoute } from "_/api";

export default defineRoute<"users/[id]">(({ GET }) => [
  GET(async (ctx) => {
    const { id } = ctx.params;
    ctx.body = { id, name: "Jane Smith", email: "jane@example.com" };
  }),
]);

Visit http://localhost:4556/api/users/123. You should see JSON.

Create a page

With the dev server still running, create pages/users/[id]/index.tsx (or .vue / .svelte / .mdx). KosmoJS generates a placeholder component - replace it with a page that fetches from your API route. React, SolidJS, and Vue fetch in the component here; Svelte and MDX read through a loader export instead (resolved before render), so they need no loading state:

tsx
import { useState, useEffect } from "react";
import { useParams } from "react-router";
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];

export default function UserPage() {
  const { id } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => { GET([id]).then(setUser); }, [id]);

  return user
    ? <div><h1>{user.name}</h1><p>{user.email}</p></div>
    : <div>Loading...</div>;
}
tsx
import { useParams } from "@solidjs/router";
import { createAsync } from "@solidjs/router";
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];

export default function UserPage() {
  const params = useParams();
  const user = createAsync(() => GET([params.id]));

  return user()
    ? <div><h1>{user().name}</h1><p>{user().email}</p></div>
    : <div>Loading...</div>;
}
vue
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];
const route = useRoute();
const user = ref(null);

onMounted(async () => { user.value = await GET([route.params.id]); });
</script>

<template>
  <div v-if="user"><h1>{{ user.name }}</h1><p>{{ user.email }}</p></div>
  <div v-else>Loading...</div>
</template>
svelte
<script module lang="ts">
import fetchClients from "_/fetch";

const { GET } = fetchClients["users/[id]"];

export const loader = ({ params }) => GET([params.id]);
</script>

<script lang="ts">
import { useLoaderData } from "_/use";

const user = useLoaderData();
</script>

<div><h1>{user.name}</h1><p>{user.email}</p></div>
mdx
import fetchClients from "_/fetch";
import { useLoaderData } from "_/use";

const { GET } = fetchClients["users/[id]"];

export const loader = ({ params }) => GET([params.id]);

export const UserPage = () => {
  const user = useLoaderData();
  return <div><h1>{user.name}</h1><p>{user.email}</p></div>;
};

<UserPage />

Visit http://localhost:4556/users/123. Your page renders with data from the API.

The fetch client is fully typed - user.name and user.email autocomplete in your editor, and invalid parameters are caught before the request leaves the browser.

What just happened

Your folder structure became your routes:

api/users/[id]/index.ts     ➜  /api/users/:id
pages/users/[id]/index.tsx  ➜  /users/:id

[id] is a required parameter. {id} makes it optional. {...path} matches any depth. The parallel structure between api/ and pages/ is intentional - API endpoints and their corresponding pages are always easy to find.

The fetch client was generated automatically from your API route definition. Change the API types, and the client updates with them - no manual sync.


That's the foundation. From here:

  • Tutorial - validation, middleware, fetch clients, pages, SSR
  • Routing - parameters, mixed segments, power syntax
  • Features - everything KosmoJS provides, at a glance

Released under the MIT License.