Backend Integration
Apinni is framework-agnostic and works seamlessly with popular Node.js backend frameworks. This guide shows you how to integrate Apinni with Express, NestJS, Fastify, and Hono.
Framework Agnostic: Apinni decorators are purely for type generation and don't affect your runtime code. You can use them alongside your framework's existing decorators and routing.
Express
Express is a minimal and flexible Node.js web application framework. Here's how to use Apinni with Express:
Basic Setup
import { Router } from 'express';
import { ApinniController, ApinniEndpoint } from '@apinni/client-ts';
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
@ApinniController({ path: '/api/users' })
export class UserController {
router = Router();
constructor() {
this.initializeRoutes();
}
private initializeRoutes() {
this.router.get('/:id', this.getUserById);
this.router.post('/', this.createUser);
this.router.put('/:id', this.updateUser);
this.router.delete('/:id', this.deleteUser);
}
@ApinniEndpoint({ path: '/:id', method: 'GET' })
private async getUserById(req, res): Promise<User> {
const user: User = {
id: req.params.id,
name: 'Alice',
email: 'alice@example.com'
};
return res.json(user);
}
@ApinniEndpoint<{
request: { type: CreateUserRequest; name: 'CreateUserDto' };
responses: { 200: { type: User; name: 'UserDto' } };
}>({ path: '/', method: 'POST' })
private async createUser(req, res): Promise<User> {
const body: CreateUserRequest = req.body;
const user: User = {
id: '123',
name: body.name,
email: body.email
};
return res.json(user);
}
@ApinniEndpoint<{
request: { type: Partial<User>; name: 'UpdateUserDto' };
responses: { 200: { type: User } };
}>({ path: '/:id', method: 'PUT' })
private async updateUser(req, res): Promise<User> {
const user: User = {
id: req.params.id,
...req.body
};
return res.json(user);
}
@ApinniEndpoint({ path: '/:id', method: 'DELETE' })
private async deleteUser(req, res): Promise<void> {
return res.status(204).send();
}
}
Registering Controllers
import express from 'express';
import { UserController } from './controllers/user.controller';
const app = express();
app.use(express.json());
// Register controllers
const userController = new UserController();
app.use('/api/users', userController.router);
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Pattern: Keep your Express routing logic separate from Apinni decorators. The decorators are only for type generation, while Express handles the actual routing.
NestJS
NestJS already uses decorators for routing. You can use Apinni decorators alongside NestJS decorators:
Basic Setup
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
import { ApinniController, ApinniEndpoint } from '@apinni/client-ts';
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserDto {
name: string;
email: string;
}
@Controller('users')
@ApinniController({ path: '/api/users' })
export class UsersController {
@Get(':id')
@ApinniEndpoint({ path: '/:id', method: 'GET' })
async getUserById(@Param('id') id: string): Promise<User> {
return {
id,
name: 'Alice',
email: 'alice@example.com'
};
}
@Post()
@ApinniEndpoint<{
request: { type: CreateUserDto };
responses: { 200: { type: User } };
}>({ path: '/', method: 'POST' })
async createUser(@Body() createUserDto: CreateUserDto): Promise<User> {
return {
id: '123',
...createUserDto
};
}
@Put(':id')
@ApinniEndpoint<{
request: { type: Partial<User> };
responses: { 200: { type: User } };
}>({ path: '/:id', method: 'PUT' })
async updateUser(
@Param('id') id: string,
@Body() updateUserDto: Partial<User>
): Promise<User> {
return {
id,
name: 'Updated',
email: 'updated@example.com',
...updateUserDto
};
}
@Delete(':id')
@ApinniEndpoint({ path: '/:id', method: 'DELETE' })
async deleteUser(@Param('id') id: string): Promise<void> {
// Delete logic
}
}
Dual Decorators: You'll use both NestJS decorators (for runtime routing) and Apinni decorators (for type generation). This gives you the best of both worlds.
With NestJS DTOs
You can leverage NestJS's class-validator DTOs with Apinni:
import { IsString, IsEmail } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
@ApinniController({ path: '/api/users' })
export class UsersController {
@Post()
@ApinniEndpoint<{
request: { type: CreateUserDto; name: 'CreateUserRequest' };
}>({ path: '/', method: 'POST' })
async createUser(@Body() dto: CreateUserDto): Promise<User> {
// NestJS validates the DTO at runtime
// Apinni generates types for the frontend
return { id: '123', ...dto };
}
}
Fastify
Fastify is a fast and low-overhead web framework. Here's how to integrate with Apinni:
Basic Setup
import { FastifyInstance } from 'fastify';
import { ApinniController, ApinniEndpoint } from '@apinni/client-ts';
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
@ApinniController({ path: '/api/users' })
export class UserController {
@ApinniEndpoint({ path: '/:id', method: 'GET' })
async getUserById(id: string): Promise<User> {
return {
id,
name: 'Alice',
email: 'alice@example.com'
};
}
@ApinniEndpoint<{
request: { type: CreateUserRequest };
responses: { 200: { type: User } };
}>({ path: '/', method: 'POST' })
async createUser(data: CreateUserRequest): Promise<User> {
return {
id: '123',
...data
};
}
registerRoutes(fastify: FastifyInstance) {
fastify.get('/api/users/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const user = await this.getUserById(id);
return reply.send(user);
});
fastify.post('/api/users', async (request, reply) => {
const body = request.body as CreateUserRequest;
const user = await this.createUser(body);
return reply.send(user);
});
}
}
Registering Routes
import Fastify from 'fastify';
import { UserController } from './controllers/user.controller';
const fastify = Fastify({ logger: true });
// Register controllers
const userController = new UserController();
userController.registerRoutes(fastify);
fastify.listen({ port: 3000 }, (err, address) => {
if (err) throw err;
console.log(`Server listening on ${address}`);
});
Hono
Hono is a small, simple, and ultrafast web framework. Here's how to use it with Apinni:
Basic Setup
import { Hono } from 'hono';
import { ApinniController, ApinniEndpoint } from '@apinni/client-ts';
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
@ApinniController({ path: '/api/users' })
export class UserController {
app = new Hono();
constructor() {
this.initializeRoutes();
}
private initializeRoutes() {
this.app.get('/:id', (c) => this.getUserById(c));
this.app.post('/', (c) => this.createUser(c));
}
@ApinniEndpoint({ path: '/:id', method: 'GET' })
private async getUserById(c): Promise<Response> {
const id = c.req.param('id');
const user: User = {
id,
name: 'Alice',
email: 'alice@example.com'
};
return c.json(user);
}
@ApinniEndpoint<{
request: { type: CreateUserRequest };
responses: { 200: { type: User } };
}>({ path: '/', method: 'POST' })
private async createUser(c): Promise<Response> {
const body: CreateUserRequest = await c.req.json();
const user: User = {
id: '123',
...body
};
return c.json(user);
}
}
Registering Controllers
import { Hono } from 'hono';
import { UserController } from './controllers/user.controller';
const app = new Hono();
// Register controllers
const userController = new UserController();
app.route('/api/users', userController.app);
export default app;
Best Practices
1. Separate Concerns
Keep your routing logic separate from type definitions:
// ✅ Good: Clear separation
@ApinniController({ path: '/api/users' })
export class UserController {
@Get(':id') // Framework routing
@ApinniEndpoint({ path: '/:id', method: 'GET' }) // Type generation
async getUserById() { }
}
2. Use Explicit Types
Always define interfaces for complex types:
// ✅ Good: Explicit interface
interface CreateUserRequest {
name: string;
email: string;
age?: number;
}
@ApinniEndpoint<{
request: { type: CreateUserRequest };
}>({ path: '/', method: 'POST' })
async createUser(data: CreateUserRequest) { }
3. Organize by Feature
Structure your controllers by feature or domain:
src/
├── users/
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── dto/
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── posts/
│ ├── posts.controller.ts
│ └── posts.service.ts
└── app.ts
4. Use Domains for Large Projects
For large projects, use @ApinniDomain to organize generated types:
@ApinniDomain({ domains: ['admin'] })
@ApinniController({ path: '/api/admin/users' })
export class AdminUserController { }
@ApinniDomain({ domains: ['public'] })
@ApinniController({ path: '/api/users' })
export class PublicUserController { }
This generates separate type files: admin-types.d.ts and public-types.d.ts.
Error Handling
Handle errors consistently across your API:
@ApinniEndpoint<{
responses: {
200: { type: User };
404: { type: { message: string } };
500: { type: { message: string; error: string } };
};
}>({ path: '/:id', method: 'GET' })
async getUserById(id: string) {
try {
const user = await this.userService.findById(id);
if (!user) {
return { status: 404, body: { message: 'User not found' } };
}
return { status: 200, body: user };
} catch (error) {
return {
status: 500,
body: { message: 'Internal server error', error: error.message }
};
}
}
Monorepo Setup
For monorepo projects, share types between packages:
packages/
├── backend/
│ ├── src/
│ │ └── controllers/
│ ├── apinni.config.ts
│ └── package.json
├── frontend/
│ ├── src/
│ │ └── api/
│ └── package.json
└── shared/
└── types/ # Generated types go here
└── api-types.d.ts
Configure Apinni to output to the shared package:
export default {
outputPath: '../shared/types',
plugins: [],
};
Next Steps
- Frontend Integration - Consume your types in the frontend
- Utility Types - Learn about generated utility types
- Best Practices - Advanced patterns and tips
- Plugins - Extend Apinni with plugins