Skip to content

Commit c769f76

Browse files
authored
Merge pull request #83 from stainless-api/cj/hono
feat: new hono package
2 parents a8209b7 + 7ccc91a commit c769f76

File tree

8 files changed

+483
-0
lines changed

8 files changed

+483
-0
lines changed

packages/hono/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Changelog

packages/hono/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# @stl-api/hono: Hono plugin for Stainless API
2+
3+
Use this plugin to serve a Stainless API in a Hono app.
4+
5+
# Getting started
6+
7+
> **Warning**
8+
>
9+
> This is alpha software, and we may make significant changes in the coming months.
10+
> We're eager for you to try it out and let us know what you think!
11+
12+
## Installation
13+
14+
```
15+
npm i --save stainless-api/stl-api#hono-0.1.0
16+
```
17+
18+
## Creating a Hono app
19+
20+
```ts
21+
import { stlApi } from "@stl-api/hono";
22+
import { Hono } from "hono";
23+
import api from "./api";
24+
25+
const app = new Hono();
26+
app.use("*", stlApi(api));
27+
```

packages/hono/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@stl-api/hono",
3+
"version": "0.1.0",
4+
"license": "ISC",
5+
"description": "hono plugin for stainless api",
6+
"author": "[email protected]",
7+
"repository": {
8+
"type": "git",
9+
"url": "https://github.com/stainless-api/stl-api.git",
10+
"directory": "packages/hono"
11+
},
12+
"homepage": "https://github.com/stainless-api/stl-api/tree/main/packages/hono",
13+
"bugs": {
14+
"url": "https://github.com/stainless-api/stl-api/issues"
15+
},
16+
"keywords": [
17+
"stainless",
18+
"api",
19+
"hono"
20+
],
21+
"source": "src/honoPlugin.ts",
22+
"main": "dist/honoPlugin.js",
23+
"types": "dist/honoPlugin.d.ts",
24+
"scripts": {
25+
"clean": "rimraf dist *.tsbuildinfo"
26+
},
27+
"devDependencies": {
28+
"@types/node": "^20.10.3",
29+
"@types/qs": "^6.9.10",
30+
"typescript": "^5.3.2",
31+
"vitest": "^1.3.1"
32+
},
33+
"dependencies": {
34+
"hono": "^4.0.0",
35+
"qs": "^6.11.2",
36+
"stainless": "github:stainless-api/stl-api#stainless-0.1.1"
37+
}
38+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { Hono } from "hono";
2+
import { Stl, UnauthorizedError, z } from "stainless";
3+
import { describe, expect, test } from "vitest";
4+
import { stlApi } from "./honoPlugin";
5+
6+
const stl = new Stl({ plugins: {} });
7+
8+
describe("basic routing", () => {
9+
const api = stl.api({
10+
basePath: "/api",
11+
resources: {
12+
posts: stl.resource({
13+
summary: "posts",
14+
actions: {
15+
retrieve: stl.endpoint({
16+
endpoint: "GET /api/posts/:postId",
17+
path: z.object({ postId: z.coerce.number() }),
18+
query: z.object({ expand: z.string().array().optional() }),
19+
response: z.object({ postId: z.coerce.number() }),
20+
handler: (params) => params,
21+
}),
22+
update: stl.endpoint({
23+
endpoint: "POST /api/posts/:postId",
24+
path: z.object({ postId: z.coerce.number() }),
25+
body: z.object({ content: z.string() }),
26+
response: z.object({
27+
postId: z.coerce.number(),
28+
content: z.string(),
29+
}),
30+
handler: (params) => params,
31+
}),
32+
list: stl.endpoint({
33+
endpoint: "GET /api/posts",
34+
response: z.any().array(),
35+
handler: () => [],
36+
}),
37+
},
38+
}),
39+
comments: stl.resource({
40+
summary: "comments",
41+
actions: {
42+
retrieve: stl.endpoint({
43+
endpoint: "GET /api/comments/:commentId",
44+
path: z.object({ commentId: z.coerce.number() }),
45+
response: z.object({ commentId: z.coerce.number() }),
46+
handler: (params) => params,
47+
}),
48+
update: stl.endpoint({
49+
endpoint: "POST /api/comments/:commentId",
50+
path: z.object({ commentId: z.coerce.number() }),
51+
handler: () => {
52+
throw new UnauthorizedError();
53+
},
54+
}),
55+
},
56+
}),
57+
},
58+
});
59+
60+
const app = new Hono();
61+
app.use("*", stlApi(api));
62+
63+
test("list posts", async () => {
64+
const response = await app.request("/api/posts");
65+
expect(response).toHaveProperty("status", 200);
66+
expect(await response.json()).toMatchInlineSnapshot(`
67+
[]
68+
`);
69+
});
70+
71+
test("retrieve posts", async () => {
72+
const response = await app.request("/api/posts/5");
73+
expect(response).toHaveProperty("status", 200);
74+
expect(await response.json()).toMatchInlineSnapshot(`
75+
{
76+
"postId": 5,
77+
}
78+
`);
79+
});
80+
81+
test("retrieve posts, wrong method", async () => {
82+
const response = await app.request("/api/posts/5", {
83+
method: "PUT",
84+
});
85+
expect(response).toHaveProperty("status", 405);
86+
expect(await response.json()).toMatchInlineSnapshot(`
87+
{
88+
"message": "No handler for PUT; only GET, POST.",
89+
}
90+
`);
91+
});
92+
93+
test("update posts", async () => {
94+
const response = await app.request("/api/posts/5", {
95+
method: "POST",
96+
headers: {
97+
"content-type": "application/json",
98+
},
99+
body: JSON.stringify({ content: "hello" }),
100+
});
101+
expect(response).toHaveProperty("status", 200);
102+
expect(await response.json()).toMatchInlineSnapshot(`
103+
{
104+
"content": "hello",
105+
"postId": 5,
106+
}
107+
`);
108+
});
109+
110+
test("update posts, wrong content type", async () => {
111+
const response = await app.request("/api/posts/5", {
112+
method: "POST",
113+
headers: {
114+
"content-type": "text/plain",
115+
},
116+
body: "hello",
117+
});
118+
expect(response).toHaveProperty("status", 400);
119+
expect(await response.json()).toMatchInlineSnapshot(`
120+
{
121+
"error": "bad request",
122+
"issues": [
123+
{
124+
"code": "invalid_type",
125+
"expected": "object",
126+
"message": "Required",
127+
"path": [
128+
"<stainless request body>",
129+
],
130+
"received": "undefined",
131+
},
132+
],
133+
}
134+
`);
135+
});
136+
137+
test("retrieve comments", async () => {
138+
const response = await app.request("/api/comments/3");
139+
expect(response).toHaveProperty("status", 200);
140+
expect(await response.json()).toMatchInlineSnapshot(`
141+
{
142+
"commentId": 3,
143+
}
144+
`);
145+
});
146+
147+
test("not found", async () => {
148+
const response = await app.request("/api/not-found");
149+
expect(response).toHaveProperty("status", 404);
150+
expect(await response.json()).toMatchInlineSnapshot(`
151+
{
152+
"error": "not found",
153+
}
154+
`);
155+
});
156+
157+
test("throwing inside handler", async () => {
158+
const response = await app.request("/api/comments/3", {
159+
method: "POST",
160+
});
161+
expect(response).toHaveProperty("status", 401);
162+
expect(await response.json()).toMatchInlineSnapshot(`
163+
{
164+
"error": "unauthorized",
165+
}
166+
`);
167+
});
168+
});
169+
170+
describe("hono passthrough", () => {
171+
const baseApi = stl.api({
172+
basePath: "/api",
173+
resources: {
174+
posts: stl.resource({
175+
summary: "posts",
176+
actions: {
177+
retrieve: stl.endpoint({
178+
endpoint: "GET /api/posts",
179+
handler: () => {
180+
throw new Error("arbitrary error");
181+
},
182+
}),
183+
},
184+
}),
185+
},
186+
});
187+
188+
const app = new Hono();
189+
app.use("*", stlApi(baseApi, { handleErrors: false }));
190+
app.all("/public/*", (c) => {
191+
return c.text("public content", 200);
192+
});
193+
app.notFound((c) => {
194+
return c.text("custom not found", 404);
195+
});
196+
app.onError((err, c) => {
197+
return c.text(`custom error: ${err.message}`, 500);
198+
});
199+
200+
test("public passthrough", async () => {
201+
const response = await app.request("/public/foo/bar");
202+
expect(response).toHaveProperty("status", 200);
203+
expect(await response.text()).toMatchInlineSnapshot(`"public content"`);
204+
});
205+
206+
test("not found passthrough", async () => {
207+
const response = await app.request("/api/comments");
208+
expect(response).toHaveProperty("status", 404);
209+
expect(await response.text()).toMatchInlineSnapshot(`"custom not found"`);
210+
});
211+
212+
test("error passthrough", async () => {
213+
const response = await app.request("/api/posts");
214+
expect(response).toHaveProperty("status", 500);
215+
expect(await response.text()).toMatchInlineSnapshot(
216+
`"custom error: arbitrary error"`
217+
);
218+
});
219+
});

0 commit comments

Comments
 (0)