Project Structure
Understanding the TSDIAPI project structure is essential for effective development. The project structure is automatically generated by the CLI, so you don't need to set up everything manually.
Directory Structure
A typical TSDIAPI project has the following structure:
my-api/
├── src/
│ ├── api/
│ │ └── features/ # Feature modules
│ │ └── users/ # Example feature
│ │ ├── users.module.ts
│ │ └── users.service.ts
│ ├── app.config.ts # Application configuration
│ └── main.ts # Application entry point
├── .env # Environment variables
├── .env.development # Development environment
├── .env.production # Production environment
├── bin.js # Application startup
├── loader.mjs # ES module loader
├── nodemon.json # Development server config
├── package.json
├── tsconfig.json
└── README.md
Key Components
Feature Modules
Feature modules are the core building blocks of your API. Each feature typically includes a module (controller) and service:
// src/api/features/users/users.module.ts
import { AppContext } from "@tsdiapi/server";
import { Type } from "@sinclair/typebox";
import { Container } from "typedi";
import { UsersService } from "./users.service.js";
// Define response schemas
const UserSchema = Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String()
});
const ErrorSchema = Type.Object({
error: Type.String()
});
export default function UsersModule({ useRoute }: AppContext): void {
// Create user endpoint
useRoute("users")
.post("/")
.summary("Create new user")
.description("Creates a new user in the system")
.tags(["Users"])
.body(Type.Object({
name: Type.String(),
email: Type.String(),
password: Type.String()
}))
.code(201, UserSchema)
.code(400, ErrorSchema)
.handler(async (req) => {
const userService = Container.get(UsersService);
const user = await userService.create(req.body);
return {
status: 201,
data: user
};
})
.build();
// Get user endpoint
useRoute("users")
.get("/:id")
.summary("Get user by ID")
.description("Retrieves user information by ID")
.tags(["Users"])
.params(Type.Object({
id: Type.String()
}))
.code(200, UserSchema)
.code(404, ErrorSchema)
.handler(async (req) => {
const userService = Container.get(UsersService);
const user = await userService.findById(req.params.id);
if (!user) {
return {
status: 404,
data: { error: "User not found" }
};
}
return {
status: 200,
data: user
};
})
.build();
}
Services
Services contain your business logic and are managed through TypeDI dependency injection:
// src/api/features/users/users.service.ts
import { Service } from "typedi";
interface User {
id: string;
name: string;
email: string;
password: string;
}
@Service()
export class UsersService {
private users: User[] = [];
async create(userData: Omit<User, "id">): Promise<Omit<User, "password">> {
const user = {
id: crypto.randomUUID(),
...userData
};
this.users.push(user);
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
async findById(id: string): Promise<Omit<User, "password"> | null> {
const user = this.users.find(u => u.id === id);
if (!user) return null;
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
}
Configuration Files
Application Config (app.config.ts)
The configuration schema defines and validates your environment variables:
// src/app.config.ts
import { Type } from "@sinclair/typebox";
export const ConfigSchema = Type.Object({
PORT: Type.Number({
default: 3000,
minimum: 1,
maximum: 65535
}),
HOST: Type.String({
default: "localhost"
})
});
export type ConfigType = Static<typeof ConfigSchema>;
Main Application File (main.ts)
The entry point sets up your TSDIAPI application:
// src/main.ts
import { createApp } from "@tsdiapi/server";
import { ConfigType, ConfigSchema } from "./app.config.js";
async function bootstrap() {
const app = await createApp<ConfigType>({
configSchema: ConfigSchema
});
console.log(`Server running at http://${app.config.HOST}:${app.config.PORT}`);
}
bootstrap().catch(console.error);
Environment Files
Environment files store your configuration for different environments:
# .env.development
PORT=3000
HOST=localhost
NODE_ENV=development
# .env.production
PORT=80
HOST=api.example.com
NODE_ENV=production
Best Practices
-
Feature Organization
- Group related functionality in feature modules
- Keep services focused on business logic
- Use TypeDI for dependency injection
- Follow single responsibility principle
-
Type Safety
- Use TypeBox for request/response validation
- Define clear interfaces for your data models
- Leverage TypeScript decorators
- Always specify return types for methods
-
Configuration Management
- Use environment-specific .env files
- Define strict types for configuration
- Set sensible defaults
- Validate configuration at startup
-
API Documentation
- Use summary and description for each route
- Group related endpoints with tags
- Document response codes
- Provide clear parameter descriptions
Next Steps
- Learn about Routing
- Explore Dependency Injection
- Check out Environment Configuration