rab-api
v1.2.0
Published
A TypeScript REST API framework built on Express.js with decorator-based routing, dependency injection, and built-in validation.
Downloads
31
Readme
rab-api
A TypeScript REST API framework built on Express.js with decorator-based routing, dependency injection, and built-in validation.
Features
- 🎯 Decorator-based routing with TypeScript
- 🔒 Built-in JWT authentication
- ✅ Request validation with Joi schemas
- 💉 Dependency injection (TypeDI)
- 🔐 Role-based access control
- 📝 Full TypeScript type safety
- 🚀 Production-ready
Installation
npm install rab-apiPeer dependencies:
npm install express joi typedi reflect-metadata jsonwebtoken compose-middlewareQuick Start
1. Create a controller:
import { Get, RabApiGet, GetController } from 'rab-api';
type ControllerT = GetController<{ status: string }>;
@Get('/health')
export class HealthCheck implements RabApiGet<ControllerT> {
handler: ControllerT['request'] = async () => {
return { status: 'ok' };
};
}2. Bootstrap your app:
import { RabApi } from 'rab-api';
import express from 'express';
const app = RabApi.createApp({
auth: {
jwt: {
secret_key: process.env.JWT_SECRET!,
algorithms: ['HS256'],
},
},
});
app.use(express.json());
app.route({
basePath: '/api',
controllers: [HealthCheck],
});
app.listen(3000);Core Concepts
Controllers
Controllers handle HTTP requests using decorators:
import { Post, RabApiPost, PostController } from 'rab-api';
import * as Joi from 'joi';
type CreateUserBody = { name: string; email: string };
type UserResponse = { id: string; name: string; email: string };
type ControllerT = PostController<CreateUserBody, UserResponse>;
const schema = Joi.object({
name: Joi.string().required(),
email: Joi.string().email().required(),
});
@Post('/users', { bodySchema: schema })
export class CreateUser implements RabApiPost<ControllerT> {
handler: ControllerT['request'] = async (request) => {
return { id: '1', ...request.body };
};
}Available decorators:
@Get(path, options?)- GET requests@Post(path, options?)- POST requests@Put(path, options?)- PUT requests@Patch(path, options?)- PATCH requests@Delete(path, options?)- DELETE requests@Query(path, options?)- GET with advanced query parsing
Dependency Injection
Controllers support constructor injection via TypeDI:
import { Injectable } from 'rab-api';
@Injectable()
class UserService {
async findAll() {
return [];
}
}
@Get('/users')
export class ListUsers implements RabApiGet<ControllerT> {
constructor(private userService: UserService) {}
handler: ControllerT['request'] = async () => {
return await this.userService.findAll();
};
}Routing
Group related controllers with routers:
app.route({
basePath: '/users',
controllers: [ListUsers, CreateUser, UpdateUser, DeleteUser],
});
// Nested routes
app.route({
basePath: '/users',
controllers: [
ListUsers,
RabApi.createRouter({
basePath: '/:userId/posts',
controllers: [ListPosts, CreatePost],
}),
],
});Validation
Request Body
const createProductSchema = Joi.object({
name: Joi.string().min(3).required(),
price: Joi.number().positive().required(),
});
@Post('/products', { bodySchema: createProductSchema })
export class CreateProduct implements RabApiPost<ControllerT> {
handler: ControllerT['request'] = async (request) => {
const { name, price } = request.body; // validated
return { id: '1', name, price };
};
}Query Parameters
const listSchema = Joi.object({
page: Joi.number().integer().min(1).default(1),
limit: Joi.number().integer().min(1).max(100).default(10),
});
@Get('/products', { querySchema: listSchema })
export class ListProducts implements RabApiGet<ControllerT> {
handler: ControllerT['request'] = async (request) => {
const { page, limit } = request.query; // validated
return { items: [], page, limit };
};
}Authentication
JWT Setup
const app = RabApi.createApp({
auth: {
jwt: {
secret_key: process.env.JWT_SECRET!,
algorithms: ['HS256'],
},
},
});Protected Routes
Routes are protected by default. Make a route public:
@Post('/auth/login', { isProtected: false })
export class Login implements RabApiPost<ControllerT> {
// Public endpoint
}Access authenticated user:
@Get('/profile')
export class GetProfile implements RabApiGet<ControllerT> {
handler: ControllerT['request'] = async (request) => {
const user = request.auth; // JWT payload
return { userId: user.userId };
};
}Authorization
Use permission-based access control:
@Post('/admin/users', { permission: 'canCreateUser' })
export class CreateUser implements RabApiPost<ControllerT> {
// Only users with 'canCreateUser' permission
}Integrate with @softin/rab-access:
import { Rab } from '@softin/rab-access';
const permissions = Rab.schema({
canCreateUser: [Rab.grant('admin'), Rab.grant('superAdmin')],
canDeleteUser: [Rab.grant('superAdmin')],
});Error Handling
Built-in exceptions:
import {
BadRequestException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
} from 'rab-api';
@Get('/users/:id')
export class GetUser implements RabApiGet<ControllerT> {
handler: ControllerT['request'] = async (request) => {
const user = await findUser(request.params.id);
if (!user) throw new NotFoundException('User not found');
return user;
};
}Custom error handler:
const app = RabApi.createApp({
errorHandler: (err, req, res, next) => {
if (err instanceof RabApiError) {
return res.status(err.statusCode).json({ error: err.message });
}
return res.status(500).json({ error: 'Internal error' });
},
});Middleware
Apply middleware at different levels:
// Route level
@Get('/users', { pipes: [loggerMiddleware] })
export class ListUsers {}
// Router level
app.route({
basePath: '/api',
pipes: [corsMiddleware, loggerMiddleware],
controllers: [/* ... */],
});
// Conditional
const conditionalAuth = (route) => {
return route.isProtected ? [authMiddleware] : [];
};
app.route({
pipes: [conditionalAuth],
controllers: [/* ... */],
});Advanced Features
Query Parsing
Use @Query for complex query parameters:
const querySchema = Joi.object({
search: Joi.string().optional(),
filters: Joi.object({
category: Joi.string(),
minPrice: Joi.number(),
}),
page: Joi.number().default(1),
});
@Query('/products', { querySchema })
export class BrowseProducts implements RabApiGet<ControllerT> {
handler: ControllerT['request'] = async (request) => {
const { search, filters, page } = request.query;
return { items: await search({ filters }), page };
};
}Controller Type Helpers
// Type helpers for controllers
PostController<TBody, TResponse, TParams?, TUser?, TQuery?>
GetController<TResponse, TQuery?, TParams?, TUser?>
PutController<TBody, TResponse, TParams?, TUser?, TQuery?>
PatchController<TBody, TResponse, TParams?, TUser?, TQuery?>
DeleteController<TParams?, TUser?>
// Interface implementations
RabApiPost<T> | AtomApiPost<T>
RabApiGet<T> | AtomApiGet<T>
RabApiPut<T> | AtomApiPut<T>
RabApiPatch<T> | AtomApiPatch<T>
RabApiDelete<T> | AtomApiDelete<T>Route Options
interface RouteOptions {
bodySchema?: Joi.ObjectSchema; // Body validation
querySchema?: Joi.ObjectSchema; // Query validation
isProtected?: boolean; // JWT required (default: true)
permission?: string; // Permission name
pipes?: Function[]; // Middleware
excludeFromDocs?: boolean; // Hide from OpenAPI
tags?: string[]; // OpenAPI tags
}Complete Example
import { Get, Post, Put, Delete, RabApi, Injectable } from 'rab-api';
import * as Joi from 'joi';
// Service
@Injectable()
class ProductService {
async findAll() { return []; }
async create(data: any) { return { id: '1', ...data }; }
}
// Controllers
const createSchema = Joi.object({
name: Joi.string().required(),
price: Joi.number().required(),
});
@Get('/')
class ListProducts {
constructor(private service: ProductService) {}
handler = async () => await this.service.findAll();
}
@Post('/', { bodySchema: createSchema })
class CreateProduct {
constructor(private service: ProductService) {}
handler = async (req) => await this.service.create(req.body);
}
// App
const app = RabApi.createApp({
auth: { jwt: { secret_key: 'secret', algorithms: ['HS256'] } },
});
app.use(express.json());
app.route({
basePath: '/products',
controllers: [ListProducts, CreateProduct],
});
app.listen(3000);License
MIT © Softin Hub
Support
Email: [email protected]
