Project Structure
Learn how to organize your RamAPI project for scalability and maintainability. This guide covers recommended folder structures, configuration files, and best practices.
Basic Project Structure
For small to medium applications:
my-ramapi-app/
├── src/
│ ├── index.ts # Application entry point
│ ├── routes/ # Route handlers
│ │ ├── users.ts
│ │ ├── posts.ts
│ │ └── auth.ts
│ ├── middleware/ # Custom middleware
│ │ ├── auth.ts
│ │ └── validation.ts
│ ├── models/ # Data models/types
│ │ ├── user.ts
│ │ └── post.ts
│ ├── services/ # Business logic
│ │ ├── user.service.ts
│ │ └── email.service.ts
│ └── utils/ # Utility functions
│ ├── db.ts
│ └── logger.ts
├── tests/ # Test files
│ ├── routes/
│ └── services/
├── dist/ # Compiled JavaScript (generated)
├── .env # Environment variables
├── .env.example # Environment template
├── .gitignore
├── package.json
├── tsconfig.json
└── README.mdEnterprise Project Structure
For large-scale applications with microservices:
my-ramapi-enterprise/
├── src/
│ ├── index.ts # Entry point
│ ├── app.ts # App setup
│ ├── server.ts # Server configuration
│ │
│ ├── config/ # Configuration
│ │ ├── index.ts
│ │ ├── database.ts
│ │ ├── observability.ts
│ │ └── security.ts
│ │
│ ├── api/ # API layer
│ │ ├── v1/ # Versioned API
│ │ │ ├── index.ts
│ │ │ ├── users/
│ │ │ │ ├── routes.ts
│ │ │ │ ├── controller.ts
│ │ │ │ ├── service.ts
│ │ │ │ └── validation.ts
│ │ │ ├── posts/
│ │ │ └── auth/
│ │ └── v2/
│ │
│ ├── middleware/ # Custom middleware
│ │ ├── authentication.ts
│ │ ├── authorization.ts
│ │ ├── error-handler.ts
│ │ └── request-logger.ts
│ │
│ ├── models/ # Data models
│ │ ├── user.model.ts
│ │ ├── post.model.ts
│ │ └── index.ts
│ │
│ ├── services/ # Business logic
│ │ ├── user.service.ts
│ │ ├── auth.service.ts
│ │ ├── email.service.ts
│ │ └── cache.service.ts
│ │
│ ├── repositories/ # Data access layer
│ │ ├── user.repository.ts
│ │ └── post.repository.ts
│ │
│ ├── database/ # Database
│ │ ├── connection.ts
│ │ ├── migrations/
│ │ └── seeds/
│ │
│ ├── types/ # TypeScript types
│ │ ├── api.types.ts
│ │ ├── auth.types.ts
│ │ └── index.ts
│ │
│ ├── utils/ # Utilities
│ │ ├── logger.ts
│ │ ├── validators.ts
│ │ └── helpers.ts
│ │
│ └── protocols/ # Multi-protocol support
│ ├── graphql/
│ │ ├── schema.ts
│ │ └── resolvers.ts
│ └── grpc/
│ ├── protos/
│ └── services.ts
│
├── tests/ # Tests
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── scripts/ # Build/deploy scripts
│ ├── build.sh
│ └── deploy.sh
│
├── docs/ # Documentation
├── dist/ # Compiled output
├── .env
├── .env.example
├── .env.test
├── .env.production
├── .gitignore
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── package.json
├── tsconfig.json
└── README.mdEntry Point Setup
src/index.ts
Main entry point that starts the server:
import { app } from './app.js';
import { config } from './config/index.js';
const PORT = config.port || 3000;
const HOST = config.host || '0.0.0.0';
app.listen(PORT, HOST, () => {
console.log(`🚀 Server running on http://${HOST}:${PORT}`);
console.log(`📊 Environment: ${process.env.NODE_ENV || 'development'}`);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully...');
await app.close();
process.exit(0);
});src/app.ts
Application setup and configuration:
import { createApp } from 'ramapi';
import { config } from './config/index.js';
import { registerMiddleware } from './middleware/index.js';
import { registerRoutes } from './api/v1/index.js';
export const app = createApp({
observability: config.observability,
onError: async (error, ctx) => {
console.error('Error:', error);
const statusCode = error instanceof HTTPError ? error.statusCode : 500;
ctx.json({ error: true, message: error.message }, statusCode);
},
});
// Register global middleware
registerMiddleware(app);
// Register routes
registerRoutes(app);
export default app;Configuration Files
config/index.ts
Centralized configuration management:
import { config as dotenvConfig } from 'dotenv';
dotenvConfig();
export const config = {
// Server
port: parseInt(process.env.PORT || '3000', 10),
host: process.env.HOST || '0.0.0.0',
env: process.env.NODE_ENV || 'development',
// Database
database: {
url: process.env.DATABASE_URL!,
maxConnections: parseInt(process.env.DB_MAX_CONNECTIONS || '10', 10),
},
// JWT
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: parseInt(process.env.JWT_EXPIRES_IN || '86400', 10),
},
// CORS
cors: {
origin: process.env.CORS_ORIGIN?.split(',') || ['*'],
credentials: true,
},
// Rate Limiting
rateLimit: {
maxRequests: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW || '60000', 10),
},
// Observability
observability: {
tracing: {
enabled: process.env.TRACING_ENABLED === 'true',
exporter: process.env.TRACING_EXPORTER || 'console',
serviceName: process.env.SERVICE_NAME || 'ramapi-service',
},
},
};.env.example
Environment variables template:
# Server Configuration
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
DB_MAX_CONNECTIONS=10
# JWT Authentication
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_EXPIRES_IN=86400
# CORS
CORS_ORIGIN=http://localhost:3001,https://myapp.com
# Rate Limiting
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000
# Observability
TRACING_ENABLED=true
TRACING_EXPORTER=console
SERVICE_NAME=my-api
# External Services
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user@example.com
SMTP_PASS=password
# Redis Cache
REDIS_URL=redis://localhost:6379Route Organization
Flat Route Structure (Small Apps)
// src/routes/users.ts
import { Router } from 'ramapi';
import { validate } from 'ramapi/middleware';
import { createUserSchema } from '../validation/user.js';
export const userRoutes = (router: Router) => {
router.get('/users', listUsers);
router.get('/users/:id', getUser);
router.post('/users', validate({ body: createUserSchema }), createUser);
router.put('/users/:id', updateUser);
router.delete('/users/:id', deleteUser);
};Modular Route Structure (Large Apps)
// src/api/v1/users/routes.ts
import { Router } from 'ramapi';
import { UserController } from './controller.js';
import { userValidation } from './validation.js';
import { authenticate } from '../../../middleware/authentication.js';
export const registerUserRoutes = (router: Router) => {
const controller = new UserController();
router.group('/users', (users) => {
users.use(authenticate);
users.get('/', controller.list);
users.get('/:id', controller.get);
users.post('/', userValidation.create, controller.create);
users.put('/:id', userValidation.update, controller.update);
users.delete('/:id', controller.delete);
});
};Controller Pattern
// src/api/v1/users/controller.ts
import { Context } from 'ramapi';
import { UserService } from './service.js';
export class UserController {
private service: UserService;
constructor() {
this.service = new UserService();
}
list = async (ctx: Context) => {
const users = await this.service.findAll();
ctx.json({ users });
};
get = async (ctx: Context) => {
const user = await this.service.findById(ctx.params.id);
ctx.json({ user });
};
create = async (ctx: Context) => {
const user = await this.service.create(ctx.body);
ctx.json({ user }, 201);
};
update = async (ctx: Context) => {
const user = await this.service.update(ctx.params.id, ctx.body);
ctx.json({ user });
};
delete = async (ctx: Context) => {
await this.service.delete(ctx.params.id);
ctx.status(204);
};
}Service Layer
Separate business logic from route handlers:
// src/services/user.service.ts
import { UserRepository } from '../repositories/user.repository.js';
import { User, CreateUserDTO } from '../types/user.types.js';
import { HTTPError } from 'ramapi';
export class UserService {
private repository: UserRepository;
constructor() {
this.repository = new UserRepository();
}
async findAll(): Promise<User[]> {
return this.repository.findAll();
}
async findById(id: string): Promise<User> {
const user = await this.repository.findById(id);
if (!user) {
throw new HTTPError(404, 'User not found');
}
return user;
}
async create(data: CreateUserDTO): Promise<User> {
// Business logic
const existingUser = await this.repository.findByEmail(data.email);
if (existingUser) {
throw new HTTPError(400, 'Email already exists');
}
return this.repository.create(data);
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = await this.findById(id);
return this.repository.update(id, data);
}
async delete(id: string): Promise<void> {
await this.findById(id);
await this.repository.delete(id);
}
}Middleware Organization
// src/middleware/index.ts
import { Server } from 'ramapi';
import { logger, cors, rateLimit } from 'ramapi/middleware';
import { config } from '../config/index.js';
import { errorHandler } from './error-handler.js';
import { requestId } from './request-id.js';
export const registerMiddleware = (app: Server) => {
// Request ID
app.use(requestId());
// Logging
app.use(logger());
// CORS
app.use(cors(config.cors));
// Rate limiting
app.use(rateLimit(config.rateLimit));
// Error handling (should be last)
app.use(errorHandler);
};TypeScript Configuration
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"],
"@/config/*": ["./src/config/*"],
"@/api/*": ["./src/api/*"],
"@/services/*": ["./src/services/*"],
"@/utils/*": ["./src/utils/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}Package.json Scripts
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc && tsc-alias",
"start": "node dist/index.js",
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:e2e": "vitest run tests/e2e",
"type-check": "tsc --noEmit",
"lint": "eslint src/**/*.ts",
"format": "prettier --write \"src/**/*.ts\"",
"db:migrate": "tsx scripts/migrate.ts",
"db:seed": "tsx scripts/seed.ts"
}
}Docker Configuration
Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/mydb
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
- POSTGRES_DB=mydb
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Best Practices
1. Separation of Concerns
Keep routes, controllers, services, and repositories separate.
2. Environment-based Configuration
Use different .env files for different environments:
.env.development.env.test.env.production
3. Error Handling
Centralize error handling logic.
4. Validation
Keep validation schemas close to routes or in dedicated files.
5. Type Safety
Define TypeScript types/interfaces for all data structures.
6. Testing
Organize tests to mirror your source structure.
7. Documentation
Document complex logic and API endpoints.
Anti-Patterns to Avoid
- Don't put business logic in route handlers
- Don't hardcode configuration values
- Don't mix concerns (routes, logic, data access)
- Don't ignore error handling
- Don't skip input validation
- Don't commit
.envfiles
Next Steps
Ready to dive deeper? Explore Core Concepts