Skip to content

Repository files navigation

Magik

A powerful, plugin-based Express server framework with decorator-based routing, database adapters, and opt-in middleware presets.

npm versionLicense: MITTypeScript

Features

  • Database Agnostic - Repository pattern with adapter support (Mongoose, PostgreSQL, etc.)
  • Opt-in Middleware - Presets are explicit, not auto-loaded
  • Flexible Auth - User adapter pattern for any user model
  • Plugin Architecture - Extensible plugin system with lifecycle hooks
  • Decorator Routing - Define routes using @Router, @Get, @Post, etc.
  • Type-Safe - Full TypeScript support with generics throughout

Installation

npm install @magikio/magik
# or
pnpm add @magikio/magik

Quick Start

import{MagikServer,allPresets}from'@magikio/magik';constserver=awaitMagikServer.init({name: 'my-api',port: 3000,presets: allPresets,// Opt-in to security + parser presets});console.log(`Server running on port ${server.port}`);

Configuration

import{MagikServer,allPresets,securityPreset,parserPreset}from'@magikio/magik';import{MongooseAdapter}from'@magikio/mongoose-adapter';constserver=awaitMagikServer.init({name: 'my-api',port: 3000,debug: true,mode: 'development',// Middleware presets are opt-in (not auto-loaded)presets: [securityPreset,parserPreset],// Or use allPresets for all built-in presets// Optional: Database configurationdatabase: {adapter: newMongooseAdapter(),primaryService: 'main',connectionOptions: {uri: process.env.MONGO_URI!,},},// Optional: Authentication configurationauth: {handlers: {ensureAuthenticated: (req,res,next)=>{if(req.user)returnnext();res.status(401).json({error: 'Unauthorized'});},ensureAdmin: (req,res,next)=>{if(req.user?.role==='admin')returnnext();res.status(403).json({error: 'Forbidden'});},},roleHandler: (roles)=>(req,res,next)=>{if(roles.includes(req.user?.role))returnnext();res.status(403).json({error: 'Forbidden'});},},// Optional: Pluginsplugins: [newErrorHandlingPlugin(),newGracefulShutdownPlugin(),],});

Database Adapters

Magik uses the adapter pattern for database-agnostic data access. This allows you to switch databases (e.g., from MongoDB to PostgreSQL) without changing your route handlers.

Using the Mongoose Adapter

import{MongooseAdapter}from'@magikio/mongoose-adapter';import{Schema}from'mongoose';// Define your schemaconstUserSchema=newSchema({email: {type: String,required: true},name: String,role: {type: String,default: 'user'},});// Create adapter and connectconstadapter=newMongooseAdapter<'main'>();constserver=awaitMagikServer.init({name: 'my-api',database: {
adapter,primaryService: 'main',connectionOptions: {uri: process.env.MONGO_URI!},},presets: allPresets,});// Register repositories after connectionconstuserRepo=adapter.registerRepository<User>('main','users',UserSchema);// Use in routesconstuser=awaituserRepo.findById('123');constusers=awaituserRepo.findMany({role: 'admin'},{limit: 10});awaituserRepo.create({email: 'test@example.com',name: 'Test User'});

Repository Interface

All repositories implement a common interface for portability:

interfaceIRepository<T,TId=string>{findById(id: TId): Promise<T|null>;findOne(query: Partial<T>): Promise<T|null>;findMany(query: Partial<T>,options?: QueryOptions): Promise<T[]>;create(data: Omit<T,'id'>): Promise<T>;createMany(data: Omit<T,'id'>[]): Promise<T[]>;update(id: TId,data: Partial<T>): Promise<T|null>;updateMany(query: Partial<T>,data: Partial<T>): Promise<number>;delete(id: TId): Promise<boolean>;deleteMany(query: Partial<T>): Promise<number>;count(query?: Partial<T>): Promise<number>;exists(query: Partial<T>): Promise<boolean>;}

Authentication

Basic Auth Configuration

constserver=awaitMagikServer.init({name: 'my-api',auth: {handlers: {ensureAuthenticated: (req,res,next)=>{if(req.user)returnnext();res.status(401).json({error: 'Unauthorized'});},ensureAdmin: (req,res,next)=>{if(req.user?.isAdmin)returnnext();res.status(403).json({error: 'Forbidden'});},},// For role-based auth using arraysroleHandler: (roles)=>(req,res,next)=>{constuserRoles=req.user?.roles??[];if(roles.some(r=>userRoles.includes(r)))returnnext();res.status(403).json({error: 'Forbidden'});},},presets: allPresets,});

Using User Adapters

For more complex auth scenarios, use the user adapter pattern:

import{MagikServer,IUserAdapter,createRoleMiddleware,createRoleHandlerFactory,}from'@magikio/magik';// Define your user typeinterfaceMyUser{id: string;groups: string;accessLevel: 'full'|'limited'|null;twoFactor?: {enabled: boolean;authenticated: boolean};}// Implement the adapterclassMyUserAdapterimplementsIUserAdapter<MyUser>{getRoles(user: MyUser){return[user.groups];}getPermissions(user: MyUser){returnuser.accessLevel ? [user.accessLevel] : [];}hasRole(user: MyUser,role: string){returnuser.groups===role;}hasPermission(user: MyUser,permission: string){returnuser.accessLevel===permission;}// Optional: 2FA supportrequiresTwoFactor(user: MyUser){returnuser.twoFactor?.enabled===true;}hasTwoFactorPassed(user: MyUser){returnuser.twoFactor?.authenticated===true;}}constuserAdapter=newMyUserAdapter();constserver=awaitMagikServer.init({name: 'my-api',auth: {
userAdapter,handlers: {ensureAuthenticated: createAuthenticatedMiddleware(userAdapter),ensureAdmin: createRoleMiddleware(userAdapter,['Admin','IT']),},roleHandler: createRoleHandlerFactory(userAdapter),},presets: allPresets,});

Auth Helper Functions

import{createRoleMiddleware,// Requires ANY of the specified rolescreateAllRolesMiddleware,// Requires ALL of the specified rolescreatePermissionMiddleware,// Requires ANY of the specified permissionscreateAllPermissionsMiddleware,createAuthenticatedMiddleware,createTwoFactorMiddleware,createRoleHandlerFactory,// For AuthConfig.roleHandler}from'@magikio/magik';// Example usageconstrequireAdmin=createRoleMiddleware(userAdapter,['Admin']);constrequireManager=createAllRolesMiddleware(userAdapter,['Manager','Verified']);constrequire2FA=createTwoFactorMiddleware(userAdapter);

Middleware Presets

Presets are opt-in and must be explicitly included in your configuration:

import{MagikServer,allPresets,// All built-in presetssecurityPreset,// Helmet, CORSparserPreset,// JSON, URL-encoded parsers}from'@magikio/magik';// Use all presetsconstserver=awaitMagikServer.init({name: 'my-api',presets: allPresets,});// Or pick specific onesconstserver=awaitMagikServer.init({name: 'my-api',presets: [securityPreset],// Only security, no parsers});// Or no presets (configure everything manually)constserver=awaitMagikServer.init({name: 'my-api',presets: [],});

Available Presets

PresetMiddleware
securityPresetHelmet (security headers), CORS
parserPresetJSON parser, URL-encoded parser

Additional Preset Packages

Install additional presets as needed:

# Session management
pnpm add @magikio/preset-session
# Redis session store
pnpm add @magikio/session-redis
# S3 file uploads
pnpm add @magikio/upload-s3
import{createSessionPreset}from'@magikio/preset-session';import{RedisStore}from'connect-redis';constserver=awaitMagikServer.init({name: 'my-api',presets: [securityPreset,parserPreset,createSessionPreset({store: newRedisStore({client: redisClient}),secret: process.env.SESSION_SECRET!,cookieName: 'my-app.sid',}),],});

Decorator-Based Routing

import{Router,Get,Post,Delete}from'@magikio/magik/decorators';import{createRoute}from'@magikio/magik/factories';import{z}from'zod';
@Router('/users')exportdefaultclassUserRouter{
@Get('/')publiclistUsers(){returncreateRoute({handler: async(req,res)=>{constusers=awaituserRepo.findMany({});res.json(users);},});}
@Get('/:id')publicgetUser(){returncreateRoute({auth: 'ensureAuthenticated',handler: async(req,res)=>{constuser=awaituserRepo.findById(req.params.id);if(!user)returnres.status(404).json({error: 'Not found'});res.json(user);},});}
@Post('/')publiccreateUser(){returncreateRoute({auth: 'ensureAdmin',schema: z.object({email: z.string().email(),name: z.string().min(2),role: z.enum(['user','admin']).optional(),}),handler: async(req,res)=>{constuser=awaituserRepo.create(req.body);res.status(201).json(user);},});}
@Delete('/:id')publicdeleteUser(){returncreateRoute({auth: ['Admin','IT'],// Role array - requires any of thesehandler: async(req,res)=>{awaituserRepo.delete(req.params.id);res.status(204).send();},});}}

Plugin System

import{MagikPlugin,IMagikServer}from'@magikio/magik/types';exportclassMyPluginimplementsMagikPlugin{config={name: 'my-plugin',version: '1.0.0',};asynconInstall(server: IMagikServer){console.log('Plugin installed!');}asyncbeforeStart(server: IMagikServer){// Before server starts listening}asyncafterStart(server: IMagikServer){// After server is listening}asyncbeforeShutdown(server: IMagikServer){// Before graceful shutdown}registerMiddleware(){return[{name: 'my-middleware',category: 'custom',priority: 50,handler: (req,res,next)=>{req.customData='hello';next();},}];}registerRoutes(){return{'/my-plugin': [{path: '/status',method: 'get',handler: (req,res)=>res.json({status: 'ok'}),}],};}}// Use the pluginawaitserver.use(newMyPlugin());

Built-in Plugins

import{ErrorHandlingPlugin,GracefulShutdownPlugin,RateLimiterPlugin,DebugPlugin,}from'@magikio/magik/plugins';// Error handling with pretty pages in developmentawaitserver.use(newErrorHandlingPlugin());// Graceful SIGTERM/SIGINT handlingawaitserver.use(newGracefulShutdownPlugin());// Rate limitingawaitserver.use(newRateLimiterPlugin({windowMs: 15*60*1000,// 15 minutesmax: 100,}));// Debug loggingawaitserver.use(newDebugPlugin());

Event System

// In a pluginregisterEvents(){return{beforeStart: async(server)=>{awaitinitializeConnections();},afterStart: async(server)=>{console.log(`Listening on ${server.port}`);},routesLoaded: (server)=>{const{ total }=server.routerManager.getRouteCount();console.log(`Loaded ${total} routes`);},};}// Or directlyserver.eventEngine.on('routesLoaded',()=>{console.log('Routes ready!');});

TypeScript Types

importtype{// ServerMagikServerConfig,IMagikServer,// DatabaseIMagikDatabaseAdapter,IRepository,IRepositoryRegistry,QueryOptions,// AuthAuthConfig,IUserAdapter,// MiddlewareMiddlewareConfig,MiddlewarePreset,MiddlewareCategory,// RoutesRouteDefinition,PathSegment,MagikRequest,// PluginsMagikPlugin,MagikPluginConfig,// EventsServerEvent,}from'@magikio/magik';

Migration from v0.x

If upgrading from a version with auto-loaded presets:

// Before (v0.x) - presets auto-loadedconstserver=awaitMagikServer.init({name: 'my-api',});// After (v1.x) - presets are opt-inimport{allPresets}from'@magikio/magik';constserver=awaitMagikServer.init({name: 'my-api',presets: allPresets,// Explicitly include presets});

Contributing

Contributions are welcome! Please read our Contributing Guide.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages