feat: add presentation layer
- Add Express routes configuration - Implement TaskController and StateController - Add error handler and logger middleware - Add shared DTOs and AppError utility - Set up application entry point
This commit is contained in:
parent
f306a2f5ff
commit
61846c97f7
|
|
@ -0,0 +1,51 @@
|
|||
import "dotenv/config";
|
||||
import { Hono } from "hono";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
import { prisma } from "./infrastructure/prisma/client.js";
|
||||
import { PrismaTaskRepository } from "./infrastructure/task/prisma-task.repository.js";
|
||||
import { PrismaStateRepository } from "./infrastructure/state/prisma-state.repository.js";
|
||||
import { TaskService } from "./application/task/task.service.js";
|
||||
import { StateService } from "./application/state/state.service.js";
|
||||
import { createApiRouter } from "./presentation/routes.js";
|
||||
import { errorHandler } from "./presentation/middleware/error-handler.js";
|
||||
import { logger } from "./presentation/middleware/logger.js";
|
||||
|
||||
const taskRepository = new PrismaTaskRepository(prisma);
|
||||
const stateRepository = new PrismaStateRepository(prisma);
|
||||
|
||||
const taskService = new TaskService(taskRepository, stateRepository);
|
||||
const stateService = new StateService(stateRepository);
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
app.use(
|
||||
"*",
|
||||
cors({
|
||||
origin: isProduction ? "https://emi.challenge.berand97.dev" : "*",
|
||||
})
|
||||
);
|
||||
app.use("*", logger);
|
||||
app.onError(errorHandler);
|
||||
|
||||
app.route("/api", createApiRouter(taskService, stateService));
|
||||
|
||||
app.get("/", (c) => {
|
||||
return c.json({
|
||||
message: "Task Management API",
|
||||
version: "1.0.0",
|
||||
endpoints: {
|
||||
tasks: "/api/tasks",
|
||||
states: "/api/states",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
|
||||
serve({ fetch: app.fetch, port }, (info) => {
|
||||
console.log(`Server running on http://localhost:${info.port}`);
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import type { ErrorHandler } from "hono";
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
|
||||
export const errorHandler: ErrorHandler = (err, c) => {
|
||||
console.error(`[Error]`, err);
|
||||
|
||||
const status = (typeof err === "object" && err !== null && "status" in err ? err.status : 500) as ContentfulStatusCode;
|
||||
const message = typeof err === "object" && err !== null && "message" in err ? err.message : "Internal server error";
|
||||
|
||||
return c.json({ success: false, error: message }, status);
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { MiddlewareHandler } from "hono";
|
||||
|
||||
export const logger: MiddlewareHandler = async (c, next) => {
|
||||
const start = Date.now();
|
||||
const method = c.req.method;
|
||||
const path = c.req.path;
|
||||
|
||||
await next();
|
||||
|
||||
const duration = Date.now() - start;
|
||||
const status = c.res.status;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${method} ${path} ${status} ${duration}ms`);
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Hono } from "hono";
|
||||
import type { TaskService } from "../application/task/task.service.js";
|
||||
import type { StateService } from "../application/state/state.service.js";
|
||||
import { createTaskController } from "./task/task.controller.js";
|
||||
import { createStateController } from "./state/state.controller.js";
|
||||
|
||||
export function createApiRouter(taskService: TaskService, stateService: StateService): Hono {
|
||||
const router = new Hono();
|
||||
|
||||
router.route("/tasks", createTaskController(taskService));
|
||||
router.route("/states", createStateController(stateService));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Hono } from "hono";
|
||||
import type { StateService } from "../../application/state/state.service.js";
|
||||
|
||||
export function createStateController(stateService: StateService): Hono {
|
||||
const controller = new Hono();
|
||||
|
||||
controller.get("/", async (c) => {
|
||||
const states = await stateService.getAll();
|
||||
return c.json({ success: true, data: states });
|
||||
});
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { Hono } from "hono";
|
||||
import type { TaskService } from "../../application/task/task.service.js";
|
||||
import type { CreateTaskDto, UpdateTaskDto, TransitionStateDto, AddNoteDto } from "../../shared/dto.js";
|
||||
|
||||
export function createTaskController(taskService: TaskService): Hono {
|
||||
const controller = new Hono();
|
||||
|
||||
controller.get("/", async (c) => {
|
||||
const tasks = await taskService.getAll();
|
||||
return c.json({ success: true, data: tasks });
|
||||
});
|
||||
|
||||
controller.get("/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const task = await taskService.getById(id);
|
||||
return c.json({ success: true, data: task });
|
||||
});
|
||||
|
||||
controller.post("/", async (c) => {
|
||||
const body = (await c.req.json()) as CreateTaskDto;
|
||||
|
||||
if (!body.title || !body.description || !body.dueDate) {
|
||||
return c.json({ success: false, error: "title, description and dueDate are required" }, 400);
|
||||
}
|
||||
|
||||
const task = await taskService.create(body);
|
||||
return c.json({ success: true, data: task }, 201);
|
||||
});
|
||||
|
||||
controller.put("/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const body = (await c.req.json()) as UpdateTaskDto;
|
||||
const task = await taskService.update(id, body);
|
||||
return c.json({ success: true, data: task });
|
||||
});
|
||||
|
||||
controller.delete("/:id", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
await taskService.delete(id);
|
||||
return c.json({ success: true, data: null });
|
||||
});
|
||||
|
||||
controller.patch("/:id/transition", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const body = (await c.req.json()) as TransitionStateDto;
|
||||
|
||||
if (!body.state) {
|
||||
return c.json({ success: false, error: "state is required" }, 400);
|
||||
}
|
||||
|
||||
const task = await taskService.transitionState(id, body.state);
|
||||
return c.json({ success: true, data: task });
|
||||
});
|
||||
|
||||
controller.post("/:id/notes", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const body = (await c.req.json()) as AddNoteDto;
|
||||
|
||||
if (!body.note) {
|
||||
return c.json({ success: false, error: "note is required" }, 400);
|
||||
}
|
||||
|
||||
const task = await taskService.addNote(id, body);
|
||||
return c.json({ success: true, data: task });
|
||||
});
|
||||
|
||||
controller.delete("/:id/notes/:noteIndex", async (c) => {
|
||||
const id = c.req.param("id");
|
||||
const noteIndex = parseInt(c.req.param("noteIndex"), 10);
|
||||
const task = await taskService.deleteNote(id, noteIndex);
|
||||
return c.json({ success: true, data: task });
|
||||
});
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export class AppError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = "AppError";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import type { TaskState } from "../domain/task/task.entity.js";
|
||||
|
||||
export interface CreateTaskDto {
|
||||
title: string;
|
||||
description: string;
|
||||
dueDate: string;
|
||||
notes?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateTaskDto {
|
||||
title?: string;
|
||||
description?: string;
|
||||
dueDate?: string;
|
||||
completed?: boolean;
|
||||
notes?: string[];
|
||||
}
|
||||
|
||||
export interface TransitionStateDto {
|
||||
state: TaskState;
|
||||
}
|
||||
|
||||
export interface AddNoteDto {
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
Loading…
Reference in New Issue