diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..2b504c8 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,7 @@ +NODE_ENV=development +PORT=4000 +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/horux360?schema=public" +JWT_SECRET=your-super-secret-jwt-key-min-32-chars-long-for-development +JWT_EXPIRES_IN=15m +JWT_REFRESH_EXPIRES_IN=7d +CORS_ORIGIN=http://localhost:3000 diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..ea58ac9 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,36 @@ +{ + "name": "@horux/api", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "lint": "eslint src/", + "typecheck": "tsc --noEmit", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@horux/shared": "workspace:*", + "@prisma/client": "^5.22.0", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "express": "^4.21.0", + "helmet": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "zod": "^3.23.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.0.0", + "prisma": "^5.22.0", + "tsx": "^4.19.0", + "typescript": "^5.3.0" + } +} diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 0000000..05917a0 --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,31 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import { env } from './config/env.js'; +import { errorMiddleware } from './middlewares/error.middleware.js'; + +const app = express(); + +// Security +app.use(helmet()); +app.use(cors({ + origin: env.CORS_ORIGIN, + credentials: true, +})); + +// Body parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Health check +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// API Routes (to be added) +// app.use('/api/auth', authRoutes); + +// Error handling +app.use(errorMiddleware); + +export { app }; diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts new file mode 100644 index 0000000..0771876 --- /dev/null +++ b/apps/api/src/config/env.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + PORT: z.string().default('4000'), + DATABASE_URL: z.string(), + JWT_SECRET: z.string().min(32), + JWT_EXPIRES_IN: z.string().default('15m'), + JWT_REFRESH_EXPIRES_IN: z.string().default('7d'), + CORS_ORIGIN: z.string().default('http://localhost:3000'), +}); + +const parsed = envSchema.safeParse(process.env); + +if (!parsed.success) { + console.error('❌ Invalid environment variables:', parsed.error.flatten().fieldErrors); + process.exit(1); +} + +export const env = parsed.data; diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..219e34f --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,9 @@ +import { app } from './app.js'; +import { env } from './config/env.js'; + +const PORT = parseInt(env.PORT, 10); + +app.listen(PORT, () => { + console.log(`🚀 API Server running on http://localhost:${PORT}`); + console.log(`📊 Environment: ${env.NODE_ENV}`); +}); diff --git a/apps/api/src/middlewares/error.middleware.ts b/apps/api/src/middlewares/error.middleware.ts new file mode 100644 index 0000000..9ec2382 --- /dev/null +++ b/apps/api/src/middlewares/error.middleware.ts @@ -0,0 +1,33 @@ +import type { Request, Response, NextFunction } from 'express'; + +export class AppError extends Error { + constructor( + public statusCode: number, + public message: string, + public isOperational = true + ) { + super(message); + Object.setPrototypeOf(this, AppError.prototype); + } +} + +export function errorMiddleware( + err: Error, + req: Request, + res: Response, + next: NextFunction +) { + if (err instanceof AppError) { + return res.status(err.statusCode).json({ + status: 'error', + message: err.message, + }); + } + + console.error('Unhandled error:', err); + + return res.status(500).json({ + status: 'error', + message: 'Internal server error', + }); +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..7236c99 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}