Skip to content

refactor: migrate from Express/Node.js to ElysiaJS/Bun#55

Open
supersonictw wants to merge 2 commits into
rollingfrom
elysia
Open

refactor: migrate from Express/Node.js to ElysiaJS/Bun#55
supersonictw wants to merge 2 commits into
rollingfrom
elysia

Conversation

@supersonictw

Copy link
Copy Markdown
Member

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the project from Node.js/Express to Bun/ElysiaJS and converts the codebase to TypeScript, including updates to Docker configuration, dependencies, and the test suite. Feedback identifies critical issues such as an invalid @types/node version, dead code in configuration fallbacks, and a regression in security metadata for email notifications. Additionally, recommendations were provided to optimize the Dockerfile, remove unused dependencies, update outdated npm scripts, improve type safety in plugins, and adhere to RESTful status code conventions.

Comment thread package.json Outdated
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.6.2",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The version ^25.6.2 for @types/node is invalid as there is no Node.js version 25. This will cause package installation to fail. You should use a version compatible with your target Node/Bun environment, such as ^20.0.0.

Suggested change
"@types/node": "^25.6.2",
"@types/node": "^20.14.2",

Comment thread src/index.ts Outdated
Comment on lines +49 to +50
const port = parseInt(getMust("HTTP_PORT") || "8080");
const hostname = getMust("HTTP_HOSTNAME") || "0.0.0.0";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The || "8080" and || "0.0.0.0" parts are dead code because getMust() will throw an error if the environment variable is not set, so the fallback values will never be used. To provide a fallback, you should import and use getFallback() from ./config instead.

Suggested change
const port = parseInt(getMust("HTTP_PORT") || "8080");
const hostname = getMust("HTTP_HOSTNAME") || "0.0.0.0";
const port = parseInt(getFallback("HTTP_PORT", "8080"));
const hostname = getFallback("HTTP_HOSTNAME", "0.0.0.0");

Comment thread src/routes/tokens.ts Outdated
/**
* Verify user's identity and issue an access token by a code
*/
.patch("/", async ({ body, set, error }: any) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When sending a notification email after a successful login with an email code, the sessionIp and accessUa are hardcoded to "unknown". This is a regression from the previous implementation and removes valuable security information from the notification email. You should pass request and server to the handler's context to get the real IP address and User-Agent.

For example, change the handler signature to async ({ body, set, error, request, server }: any) and then use getIPAddress(request, server) and getUserAgent(request.headers, true).

Comment thread Dockerfile Outdated
Comment on lines +7 to +14
RUN addgroup \
-g 3000 \
recv
RUN adduser -HD \
-u 3000 \
-G recv \
-h /app \
recv

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The addgroup and adduser commands can be combined into a single RUN instruction to reduce the number of layers in the Docker image. This is a common optimization practice.

RUN addgroup -g 3000 recv && \
    adduser -HD -u 3000 -G recv -h /app recv

Comment thread package.json Outdated
@@ -7,23 +7,27 @@
"main": "app.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The main field still points to app.js, which has been deleted in this refactoring. While it might not be used since you're running the app with bun src/index.ts, it's good practice to keep this field accurate. You could either remove it or point it to the new entry point.

Suggested change
"main": "app.js",
"main": "src/index.ts",

Comment thread package.json
Comment on lines 18 to 20
"lint-staged": {
"*.js": "eslint"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The lint-staged configuration is still set up for .js files. It should be updated to lint .ts files to be effective with the new TypeScript codebase.

Suggested change
"lint-staged": {
"*.js": "eslint"
},
"lint-staged": {
"*.ts": "eslint"
},

Comment thread package.json Outdated
Comment on lines 31 to 32
"express": "^4.18.2",
"express-validator": "^6.14.3",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dependencies express and express-validator appear to be unused after migrating to ElysiaJS. They should be removed to keep the dependency list clean and reduce the project's size.

Comment thread src/plugins/auth.ts Outdated
})
.macro(({ onBeforeHandle }) => ({
access(requiredRole: string | null) {
onBeforeHandle(({ auth, error }: { auth: AuthContext | null, error: any }) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The context object for the onBeforeHandle hook is typed as any. Elysia provides types for its lifecycle hooks (e.g., BeforeHandleContext) that can be used for better type safety and autocompletion. Using explicit types would improve the maintainability of the code. This is a general suggestion that applies to many of the new route handlers and plugins in this PR. For example: onBeforeHandle((context: BeforeHandleContext) => { ... }).

Comment thread src/routes/admin.ts Outdated
user.roles = [...user.roles, body.role_name];
await user.save();

return { message: "Role added" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This POST route creates a new resource (a role assignment). It's conventional to return a 201 Created status code in this case. The current implementation returns a default 200 OK. You can achieve this by adding set to the handler's context and calling set.status = 201;.

Comment thread src/routes/admin.ts Outdated
user.roles = user.roles.filter((name) => name !== params.role_name);
await user.save();

return { message: "Role removed" };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This DELETE route successfully removes a resource. It's conventional to return a 204 No Content status code with an empty body. The current implementation returns a 200 OK with a JSON body. You can achieve this by adding set to the handler's context, calling set.status = 204;, and removing the return statement.

@supersonictw supersonictw changed the base branch from main to rolling May 12, 2026 17:12
@supersonictw

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the application from Node.js and Express to Bun and ElysiaJS, involving a comprehensive rewrite from JavaScript to TypeScript. The changes encompass updated Dockerfiles, Mongoose models, and the implementation of Elysia-specific plugins for authentication and rate limiting. Feedback correctly identifies critical runtime issues where status is incorrectly invoked as a function instead of using set.status, which will lead to failed responses. Additionally, the review points out potential unique constraint violations during soft deletes, the use of t.Any() which compromises validation for sensitive credentials, and fragile status code parsing in the restrictor plugin.

Comment thread src/routes/admin.ts
Comment on lines +13 to +15
.get("/users/:user_id", async ({params, status}) => {
const user = await User.findById(params.user_id).exec();
if (!user) return status(404);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In ElysiaJS, status is not a function in the context; it is typically set via set.status. Calling status(404) will likely result in a runtime error. Furthermore, returning a numeric value from a handler causes Elysia to use that number as the response body while keeping the HTTP status as 200 OK. You should set set.status explicitly and return the desired response body (or nothing). This pattern is repeated in several other route files.

    .get("/users/:user_id", async ({params, set}) => {
        const user = await User.findById(params.user_id).exec();
        if (!user) {
            set.status = 404;
            return;
        }

Comment thread src/plugins/auth.ts
Comment on lines +56 to +58
onBeforeHandle(({auth, status}: any) => {
if (!auth || !auth.id) {
return status(401);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the route handlers, status is not a function in the context. In onBeforeHandle, returning a value will stop the request and use that value as the response body. To return an empty 401 response, set set.status and return nothing.

Suggested change
onBeforeHandle(({auth, status}: any) => {
if (!auth || !auth.id) {
return status(401);
onBeforeHandle(({auth, set}: any) => {
if (!auth || !auth.id) {
set.status = 401;
return;
}

Comment thread src/routes/users.ts
if (!user) return status(404);

user.nickname = APP_NAME;
user.email = new Date().toISOString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using new Date().toISOString() as a replacement for the email during soft delete may lead to unique constraint violations if multiple users are deleted at the exact same millisecond. It is safer to include the user's ID to ensure uniqueness.

Suggested change
user.email = new Date().toISOString();
user.email = `deleted-${user.id}-${Date.now()}`;

Comment thread src/routes/users.ts
body: t.Object({
session_id: t.String(),
credential: t.Any(),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using t.Any() for the credential object bypasses type safety and validation for sensitive authentication data. It is recommended to define a proper schema (similar to the one used in the /tokens/passkeys route) to ensure the input data is valid and secure.

Comment thread src/plugins/restrictor.ts
Comment on lines +68 to +69
const currentStatus = typeof set.status === "string" ?
parseInt(set.status) : set.status;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

parseInt(set.status) will return NaN if set.status is a string status name (e.g., "Not Found"), which Elysia supports. This would cause the subsequent comparison with forbiddenStatus to fail. Consider checking if the status is a number or handling string status names explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant