Initial commit

This commit is contained in:
2026-07-27 18:01:07 +02:00
commit 0758b00d54
192 changed files with 45091 additions and 0 deletions

22
apps/api/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
# This file is generated by Nx.
# Build the docker image with `npx nx docker:build api`.
# Tip: Modify "docker:build" options in project.json to change docker build args.
#
# Run the container with `nx docker:run api -p 3000:3000`.
#
FROM docker.io/node:lts-alpine
ENV HOST=0.0.0.0
ENV PORT=3333
WORKDIR /app
COPY dist .
# You can remove this install step if you build with `--bundle` option.
# The bundled output will include external dependencies.
RUN npm --omit=dev -f install
CMD [ "node", "main.js" ]

View File

@@ -0,0 +1,3 @@
import baseConfig from '../../eslint.config.mjs';
export default [...baseConfig];

73
apps/api/project.json Normal file
View File

@@ -0,0 +1,73 @@
{
"name": "api",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/api/src",
"projectType": "application",
"tags": ["scope:api"],
"release": {
"docker": {
"repositoryName": "org/angular-template-api"
}
},
"targets": {
"build": {
"executor": "@nx/esbuild:esbuild",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"platform": "node",
"outputPath": "apps/api/dist",
"format": ["cjs"],
"bundle": false,
"main": "apps/api/src/main.ts",
"tsConfig": "apps/api/tsconfig.app.json",
"assets": ["apps/api/src/assets"],
"generatePackageJson": true,
"esbuildOptions": {
"sourcemap": true,
"outExtension": {
".js": ".js"
}
}
},
"configurations": {
"development": {},
"production": {
"generateLockfile": true,
"esbuildOptions": {
"sourcemap": false,
"outExtension": {
".js": ".js"
}
}
}
}
},
"serve": {
"continuous": true,
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"dependsOn": ["build"],
"options": {
"buildTarget": "api:build",
"runBuildTargetDependencies": false
},
"configurations": {
"development": {
"buildTarget": "api:build:development"
},
"production": {
"buildTarget": "api:build:production"
}
}
},
"test": {
"options": {
"passWithNoTests": true
}
},
"docker:build": {
"dependsOn": ["build"]
}
}
}

View File

139
apps/api/src/main.ts Normal file
View File

@@ -0,0 +1,139 @@
import express from 'express';
import { ProductsService } from '@org/api/products';
import { ApiResponse, Product, ProductFilter, PaginatedResponse } from '@org/models';
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ? Number(process.env.PORT) : 3333;
const app = express();
const productsService = new ProductsService();
// Middleware
app.use(express.json());
// CORS configuration for Angular app
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
app.get('/', (req, res) => {
res.send({ message: 'Hello API' });
});
// Products endpoints
app.get('/api/products', (req, res) => {
try {
const filter: ProductFilter = {};
if (req.query.category) {
filter.category = req.query.category as string;
}
if (req.query.minPrice) {
filter.minPrice = Number(req.query.minPrice);
}
if (req.query.maxPrice) {
filter.maxPrice = Number(req.query.maxPrice);
}
if (req.query.inStock !== undefined) {
filter.inStock = req.query.inStock === 'true';
}
if (req.query.searchTerm) {
filter.searchTerm = req.query.searchTerm as string;
}
const page = req.query.page ? Number(req.query.page) : 1;
const pageSize = req.query.pageSize ? Number(req.query.pageSize) : 12;
const result = productsService.getAllProducts(filter, page, pageSize);
const response: ApiResponse<PaginatedResponse<Product>> = {
data: result,
success: true,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
data: null,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
res.status(500).json(response);
}
});
app.get('/api/products/:id', (req, res) => {
try {
const product = productsService.getProductById(req.params.id);
if (!product) {
const response: ApiResponse<null> = {
data: null,
success: false,
error: 'Product not found',
};
return res.status(404).json(response);
}
const response: ApiResponse<Product> = {
data: product,
success: true,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
data: null,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
res.status(500).json(response);
}
});
app.get('/api/products-metadata/categories', (req, res) => {
try {
const categories = productsService.getCategories();
const response: ApiResponse<string[]> = {
data: categories,
success: true,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
data: null,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
res.status(500).json(response);
}
});
app.get('/api/products-metadata/price-range', (req, res) => {
try {
const priceRange = productsService.getPriceRange();
const response: ApiResponse<{ min: number; max: number }> = {
data: priceRange,
success: true,
};
res.json(response);
} catch (error) {
const response: ApiResponse<null> = {
data: null,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
res.status(500).json(response);
}
});
app.listen(port, host, () => {
console.log(`[ ready ] http://${host}:${port}`);
});

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

16
apps/api/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}

View File

@@ -0,0 +1,28 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

22
apps/api/vite.config.ts Normal file
View File

@@ -0,0 +1,22 @@
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/api',
plugins: [tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])],
test: {
name: 'api',
watch: false,
globals: true,
environment: 'node',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
passWithNoTests: true,
coverage: {
reportsDirectory: '../../coverage/apps/api',
provider: 'v8' as const,
},
},
}));