This project has been completely rewritten and is available here
Ensure that all project dependencies are installed using:
bun install
bun dev
Copy the provided template for environment variables:
cp .env.template .env
Update the .env file with your specific configuration.
Ensure to update the Dockerfile to include your new variables.
When introducing new environment variables, validate them in src/config/config.ts
using the Zod library:
const schema = z.object({
CUSTOM_VARIABLE: withDevDefault(z.string(), "Default value"),
});
This step ensures that all variables adhere to a defined schema, enhancing the reliability of your application.
This project utilizes Inversify, an inversion of control container for JavaScript & Node.js apps powered by TypeScript.
There are several ways to inject classes, in the application we use service identifier binding and name binding.
We use service identifier binding when we need to inject a class that will remain unique within its business context.
// utils/container.ts
const container = new Container()
container.bind<AppLogger>(SERVICE_IDENTIFIER.Logger).to(AppLogger).inSingletonScope();
// src/index.ts
const appLogger = iocContainer.get<AppLogger>(SERVICE_IDENTIFIER.Logger);
// controllers/user/index.ts
// Updated file for example
@injectable()
export class UserController implements IController {
public constructor(
@inject(SERVICE_IDENTIFIER.Logger) private logger: Logger,
) { }
}
For more information about name identifier, please refer to the Wiki
We use named binding when there are multiple classes within a context. In the application, we group all controllers under the "controller" identifier (service-identifier.ts
), and then they are identified by their names within the service (service-name.ts
). You'll need to add an entry in the constant SERVICE_NAME
for this.
// utils/container.ts
const container = new Container():
container.bind<ControllerRoot>(SERVICE_IDENTIFIER.Controller).to(ControllerRoot)
.whenTargetNamed(SERVICE_NAME.controllers.root);
container.bind<UserController>(SERVICE_IDENTIFIER.Controller).to(UserController)
.whenTargetNamed(SERVICE_NAME.controllers.user);
container.bind<PostsController>(SERVICE_IDENTIFIER.Controller).to(PostsController)
.whenTargetNamed(SERVICE_NAME.controllers.posts);
// src/index.ts
const controllerRoot = iocContainer.getNamed<ControllerRoot>(SERVICE_IDENTIFIER.Controller, SERVICE_NAME.controllers.root);
// controllers/index.ts
@injectable()
export class ControllerRoot implements IController {
public constructor(
@inject(SERVICE_IDENTIFIER.Controller) @named(SERVICE_NAME.controllers.posts) private postsController: PostsController,
@inject(SERVICE_IDENTIFIER.Controller) @named(SERVICE_NAME.controllers.user) private userController: UserController,
) { }
}
For more information about name binding, please refer to the Wiki:
You can find all the information about Inversify Wiki.
Effortlessly add routes using the @Controller
decorator. In the parameters, define the configuration of your route, and in the function, simply provide the return of your route.
Note: Due to the design of the
@Controller
, it is essential to call your property App:server
for the decorator to function correctly. This allows the decorator to resolve it and create your routes.
public constructor(
@inject(SERVICE_IDENTIFIER.App) private readonly server: App,
) { }
Note: Currently, the types of your functions should be set to
any
orunknown
because the server we are using, Zod OpenAPI Hono, does not export its types. This prevents us from typing our returns, and work is underway to address this.
For additional insights, please refer to the project link provided: Zod OpenApi Hono
// Custom example
@injectable()
export class GroupsController implements IController {
public constructor(
@inject(SERVICE_IDENTIFIER.App) private readonly server: App,
) { }
@Controller({
method: 'post',
path: `${defaultPath}/{id}`,
request: {},
responses: {},
})
private async create(ctx?: hono.Context): Promise<any> {
// If the context is not defined will throw an error in the console using appLogger
isContextDefined(ctx);
if (ctx) {
const body = await ctx.req.raw.json();
// Represent the return of the route
return ctx.json({
age: 20,
name: `body: ${body}, Coucou je veux justye faire un test`,
});
}
}
}
This project utilizes Prisma, an Object-Relational Mapping (ORM) tool, to interact with the database.
Running prisma :
At the moment Prisma needs Node.js to be installed to run certain generation code. Make sure Node.js is installed in the environment where you're running bunx prisma commands.
bunx prisma
There is a class in the application (AppOrm
) that allows retrieving a Prisma instance within the application as follows:
constructor(
@inject(SERVICE_IDENTIFIER.Orm) private orm: AppOrm,
) { }
For more details, refer to the Prisma Documentation.
This project has adopted Bun Test for running tests. To run your tests, use the following command:
bun test
Create your test files and place them in the directory named src/__tests__
with a .spec.ts
extension.
// Example from file logger/index.test.ts
// ! Should be set in every tests files
import 'reflect-metadata';
import { SERVICE_IDENTIFIER } from "@config/ioc/service-identifier";
import { expect, describe, it, beforeAll } from "bun:test";
import { Container } from "inversify";
import { AppLogger } from ".";
import { bindContainer } from "@config/utils/container";
describe("AppLogger", () => {
const container = new Container();
// ! Should be set in every tests files
beforeAll(() => {
bindContainer(container);
});
it("Should initialize pino", () => {
const appLogger = container.get<AppLogger>(SERVICE_IDENTIFIER.Logger);
// Checking type
expect(appLogger).toBeInstanceOf(AppLogger);
expect(appLogger.pino).toBeDefined();
expect(appLogger.config).toBeDefined();
});
});
For more information on Bun Test and its features, refer to the Bun Test Documentation.
In the index.ts
file, there is a custom error handling.
// Error Handling
app.onError((err, c) => {
appLogger.pino.error(err);
return c.text(ReasonPhrases.INTERNAL_SERVER_ERROR, StatusCodes.INTERNAL_SERVER_ERROR);
});
To set up Sentry, we use Hono Sentry.
To configure Sentry, ensure to enhance your environment by updating the SENTRY_DSN
with your specific DSN.
// From file: index.ts
// Setup sentry
const sentryPrivate = config.get<string>('SENTRY_DSN');
app.use('*', sentry({
dsn: sentryPrivate,
tracesSampleRate: 1.0,
}));
To build your project with Bun bundler, use the following command. This will create a dist
folder containing the bundled files.
bun run build
If you need to update the build configuration, you can modify the scripts/build.js
file. Refer to the Bun Build Documentation for guidance on customizing the build process.
Build the Docker image and run the application with the following commands:
docker build --pull -t aecreator-bun .
docker run -d -p <host-port>:3000 aecreator-bun --PORT=3000 ...
Don't forget to include each environment variable.