Skip to content

Commit

Permalink
Revert "elysia overhaul"
Browse files Browse the repository at this point in the history
This reverts commit 1e8807f.
  • Loading branch information
cdleveille committed Nov 8, 2024
1 parent 1e8807f commit 7af93c5
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 90 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# fullstack-bun

Type-safe monorepo project template. [Bun](https://bun.sh)/[Elysia](https://elysiajs.com/) backend, [React](https://react.dev) frontend, [Socket.IO](https://socket.io) bridging the gap.
Type-safe monorepo project template. [Bun](https://bun.sh)/[Hono](https://hono.dev) backend, [React](https://react.dev) frontend, [Socket.IO](https://socket.io) bridging the gap.

## Features

- [Scalar](https://guides.scalar.com) documentation for API routes is served on [/reference](https://fullstack-bun.fly.dev/reference). [OpenAPI Specification](https://swagger.io/specification) raw .json data is served on [/reference/json](https://fullstack-bun.fly.dev/spec).
- Server API routes are automatically validated and documented via [@hono/zod-openapi](https://www.npmjs.com/package/@hono/zod-openapi). [OpenAPI Specification](https://swagger.io/specification) .json data is served on [/spec](https://fullstack-bun.fly.dev/spec), and [Scalar](https://guides.scalar.com) documentation is served on [/reference](https://fullstack-bun.fly.dev/reference).

- The client implements [PWA](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps) standards for a native app-like experience and boasts a [near-perfect PageSpeed Insights score](https://pagespeed.web.dev/analysis/https-fullstack-bun-fly-dev/uosoviysds?form_factor=desktop) out of the box. It instantly rebuilds when a client-side source file is saved, and the browser will automatically refresh, optionally with [persisted state](https://github.com/cdleveille/fullstack-bun/blob/main/src/client/hooks/usePersistedState.ts).

Expand All @@ -30,7 +30,7 @@ bun dev
## Stack

- [Bun](https://bun.sh) (server runtime, bundler, package manager, script runner)
- [Elysia](https://elysiajs.com/) (web framework)
- [Hono](https://hono.dev) (web framework)
- [React](https://react.dev) (user interface)
- [TanStack Query](https://tanstack.com/query) (async state management)
- [Socket.IO](https://socket.io) (real-time server/client communication)
Expand Down
Binary file modified bun.lockb
Binary file not shown.
10 changes: 4 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fullstack-bun",
"version": "1.0.0",
"description": "Type-safe monorepo project template. Bun/Elysia backend, React frontend, Socket.IO bridging the gap.",
"description": "Type-safe monorepo project template. Bun/Hono backend, React frontend, Socket.IO bridging the gap.",
"author": "Chris Leveille <[email protected]>",
"license": "MIT",
"type": "module",
Expand Down Expand Up @@ -35,11 +35,9 @@
"workbox-strategies": "^7.3.0"
},
"dependencies": {
"@elysiajs/cors": "^1.1.1",
"@elysiajs/static": "^1.1.1",
"@elysiajs/swagger": "^1.1.5",
"elysia": "^1.1.24",
"elysia-helmet": "^2.0.0",
"@hono/zod-openapi": "^0.16.4",
"@scalar/hono-api-reference": "^0.5.158",
"hono": "^4.6.8",
"socket.io": "^4.8.1"
}
}
13 changes: 13 additions & 0 deletions src/server/helpers/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type { StatusCode } from "hono/utils/http-status";

import type { OpenAPIHono } from "@hono/zod-openapi";
import type { TResMessage } from "@types";

export class CustomError extends Error {
status: number;

Expand All @@ -7,3 +12,11 @@ export class CustomError extends Error {
Object.setPrototypeOf(this, CustomError.prototype);
}
}

export const initErrorHandling = (app: OpenAPIHono) => {
app.notFound(c => c.json({ message: "Not Found" }, 404));
app.onError((e, c) => {
const errorStatus = ("status" in e && typeof e.status === "number" ? e.status : 500) as StatusCode;
return c.json<TResMessage>({ message: e.message || "Internal Server Error" }, errorStatus);
});
};
4 changes: 2 additions & 2 deletions src/server/helpers/schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { t } from "elysia";
import { z } from "@hono/zod-openapi";

export const resMessageSchema = t.Object({ message: t.String() });
export const resMessageSchema = z.object({ message: z.string() });
115 changes: 36 additions & 79 deletions src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,46 @@
import { Elysia, t, ValidationError } from "elysia";
import { helmet } from "elysia-helmet";

import { Env } from "@constants";
import { cors } from "@elysiajs/cors";
import { staticPlugin } from "@elysiajs/static";
import { swagger } from "@elysiajs/swagger";
import { Config, initSocket, resMessageSchema } from "@helpers";

import { name, version } from "../../package.json";
import { Config, initErrorHandling, initSocket } from "@helpers";
import { OpenAPIHono } from "@hono/zod-openapi";
import { initMiddleware } from "@middleware";
import { initRoutes } from "@routes";

const { IS_PROD, PORT, WS_PORT, HOST } = Config;
const WS_HOST = Config.HOST.replace("http", "ws");
const { IS_PROD, PORT } = Config;

const buildIfDev = IS_PROD ? [] : [(await import("@processes")).buildClient()];

await Promise.all([...buildIfDev, initSocket()]);

const app = new Elysia()
.onError(c => {
if (c.error instanceof ValidationError) {
return {
message: c.error.all.map(e => e.summary).join(", ")
};
}
return { message: c.error?.message ?? "Internal Server Error" };
})
.use(cors())
.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
childSrc: ["'self'"],
connectSrc: ["'self'", `${HOST}:${WS_PORT}`, `${WS_HOST}:${WS_PORT}`],
fontSrc: ["'self'", "https:", "data:"],
formAction: ["'self'"],
frameAncestors: ["'self'"],
frameSrc: ["'self'"],
imgSrc: ["'self'", "data:"],
manifestSrc: ["'self'"],
mediaSrc: ["'self'"],
objectSrc: ["'none'"],
scriptSrc: ["'self'"],
scriptSrcAttr: ["'none'"],
scriptSrcElem: ["*", "'unsafe-inline'"],
styleSrc: ["'self'", "https:", "'unsafe-inline'"],
styleSrcAttr: ["'self'", "https:", "'unsafe-inline'"],
styleSrcElem: ["'self'", "https:", "'unsafe-inline'"],
upgradeInsecureRequests: [],
workerSrc: ["'self'", "blob:"]
}
}
})
)
.use(swagger({ path: "/reference", documentation: { info: { title: name, version } } }))
.use(staticPlugin({ prefix: "/", assets: "./public" }))
.get(
"/hello",
c => {
const { name } = c.query;
return {
message: `hello ${name ? name : "world"}!`
};
},
{
query: t.Object({ name: t.Optional(t.String()) }),
response: resMessageSchema
}
)
.post(
"/hello",
c => {
const { name } = c.body;
return {
message: `hello ${name ? name : "world"}!`
};
},
{
body: t.Object({ name: t.String() }),
response: resMessageSchema
const app = new OpenAPIHono({
defaultHook: (result, c) => {
if (!result.success) {
return c.json(
{
message: result.error.errors
.map(err => {
const error = err as typeof err & { expected?: string };
const path = error.path.join(".");
const message = error.message;
const expected = error.expected ? ` <${error.expected}>` : "";
return `${path}${expected}: ${message}`;
})
.join(", ")
},
400
);
}
)
},
strict: false
});

initMiddleware(app);

initRoutes(app);

initErrorHandling(app);

.listen(PORT);
console.log(`HTTP server started on port ${PORT} in ${IS_PROD ? Env.Production : Env.Development} mode`);

const url = app?.server?.url?.toString();
console.log(`HTTP server listening on ${url} in ${IS_PROD ? Env.Production : Env.Development} mode`);
export default {
port: Config.PORT,
fetch: app.fetch
};
67 changes: 67 additions & 0 deletions src/server/middleware/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { serveStatic } from "hono/bun";
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";

import { Path } from "@constants";
import { Config } from "@helpers";
import type { OpenAPIHono } from "@hono/zod-openapi";
import { apiReference } from "@scalar/hono-api-reference";

import { name, version } from "../../../package.json";

const { WS_PORT, HOST } = Config;
const WS_HOST = Config.HOST.replace("http", "ws");

const openApiInfo = {
openapi: "3.1.0",
info: { version, title: name }
};

export const initMiddleware = (app: OpenAPIHono) => {
app.use(cors());

app.use(
secureHeaders({
contentSecurityPolicy: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
childSrc: ["'self'"],
connectSrc: ["'self'", `${HOST}:${WS_PORT}`, `${WS_HOST}:${WS_PORT}`],
fontSrc: ["'self'", "https:", "data:"],
formAction: ["'self'"],
frameAncestors: ["'self'"],
frameSrc: ["'self'"],
imgSrc: ["'self'", "data:"],
manifestSrc: ["'self'"],
mediaSrc: ["'self'"],
objectSrc: ["'none'"],
scriptSrc: ["'self'"],
scriptSrcAttr: ["'none'"],
scriptSrcElem: ["'self'", "https://cdn.jsdelivr.net/npm/@scalar/api-reference"],
styleSrc: ["'self'", "https:", "'unsafe-inline'"],
styleSrcAttr: ["'self'", "https:", "'unsafe-inline'"],
styleSrcElem: ["'self'", "https:", "'unsafe-inline'"],
upgradeInsecureRequests: [],
workerSrc: ["'self'", "blob:"]
}
})
);

app.get(
"/*",
serveStatic({
root: Path.Public,
onFound: (_path, c) => c.header("Cache-Control", "no-store")
})
);

app.doc31("/spec", openApiInfo);
app.getOpenAPI31Document(openApiInfo);

app.get(
"/reference",
apiReference({
spec: { url: "/spec" }
})
);
};
52 changes: 52 additions & 0 deletions src/server/routes/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { resMessageSchema } from "@helpers";
import { createRoute, type OpenAPIHono, z } from "@hono/zod-openapi";

export const initHelloRoutes = (app: OpenAPIHono) => {
app.openapi(
createRoute({
method: "get",
path: "/hello",
request: {
query: z.object({ name: z.string().optional() })
},
responses: {
200: {
content: { "application/json": { schema: resMessageSchema } },
description: "ok"
}
}
}),
c => {
const { name } = c.req.valid("query");
const message = `Hello ${name ? name : "World"}!`;
return c.json({ message }, 200);
}
);

app.openapi(
createRoute({
method: "post",
path: "/hello",
request: {
body: {
content: { "application/json": { schema: z.object({ name: z.string().min(1) }) } }
}
},
responses: {
200: {
content: { "application/json": { schema: resMessageSchema } },
description: "ok"
},
400: {
content: { "application/json": { schema: resMessageSchema } },
description: "bad request"
}
}
}),
c => {
const body = c.req.valid("json");
const message = `Hello ${body.name || "World"}!`;
return c.json({ message }, 200);
}
);
};
10 changes: 10 additions & 0 deletions src/server/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { OpenAPIHono } from "@hono/zod-openapi";
import { initHelloRoutes } from "@routes";

export * from "./hello";

export const initRoutes = (app: OpenAPIHono) => {
initHelloRoutes(app);

app.get("/health", c => c.text("OK"));
};
4 changes: 4 additions & 0 deletions src/types/abstract/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { SocketEvent } from "@constants";
import { resMessageSchema } from "@helpers";
import { z } from "@hono/zod-openapi";

export type TResMessage = z.infer<typeof resMessageSchema>;

export type TAppContext = {
message: string;
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"@contexts": ["./src/client/contexts"],
"@helpers": ["./src/server/helpers"],
"@hooks": ["./src/client/hooks"],
"@middleware": ["./src/server/middleware"],
"@processes": ["./processes"],
"@routes": ["./src/server/routes"],
"@types": ["./src/types/abstract"],
"@utils": ["./src/client/utils"]
}
Expand Down

0 comments on commit 7af93c5

Please sign in to comment.