Zero to a working route in under five minutes.
Create & install
npm create kosmo
cd my-app
npm installpnpm create kosmo
cd my-app
pnpm installyarn create kosmo
cd my-app
yarn installAdd a source folder
npm run +folderpnpm +folderyarn +folderYou'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
Noneas the framework. - for a client-only folder pick
Noneas 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:
npm installpnpm installyarn installStart the dev server
npm run devpnpm devyarn devCreate 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:
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" });
}),
]);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" };
}),
]);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:
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>;
}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>;
}<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><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>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: