jcc-express-starter
Version:
1,686 lines (1,213 loc) ⢠159 kB
Markdown
# JCC Express MVC Framework Documentation
Welcome to the JCC Express MVC framework documentation. JCC Express MVC is a powerful, expressive web application framework built on Express.js, designed to make web development a creative and enjoyable experience.
## š Documentation
For complete documentation, visit: **[https://www.jcc-express.uk/](https://www.jcc-express.uk/)**
---
# Introduction
JCC Express MVC is a modern, full-featured web application framework built on Express.js that follows Laravel-style architecture patterns. It is **specifically designed and optimized for the Bun runtime**, offering a familiar developer experience for those coming from the Laravel ecosystem while leveraging the exceptional performance of Bun.
The framework provides a robust set of tools and features including routing, controllers, middleware, database abstraction with Eloquent ORM, authentication, caching, queues, events, and much more. Whether you're building a simple API or a complex enterprise application, JCC Express MVC has the tools you need.
## Why JCC Express MVC?
- **Laravel-Inspired**: Familiar patterns and conventions for Laravel developers
- **Bun Optimized**: Built specifically for Bun runtime for maximum performance
- **Type-Safe**: Full TypeScript support with excellent type inference
- **Eloquent ORM**: Beautiful, expressive database interactions
- **Modern Architecture**: Service container, dependency injection, and more
- **Developer Experience**: Intuitive APIs and comprehensive tooling
## Requirements
Before you begin, ensure your machine meets the following requirements:
- **Bun runtime** (v1.0.0 or higher) - Required
- **Node.js** (v18.0.0 or higher) - For npm package management
- **Database**: MySQL 5.7+, PostgreSQL 10+, or SQLite 3.8+
- **Redis** (optional) - For caching and queue management
---
# Installation
## Prerequisites
Before using this framework, you must have Bun installed on your machine.
1. Install Bun (if not already installed):
```bash
curl -fsSL https://bun.sh/install | bash
```
2. Verify Bun installation:
```bash
bun --version
```
## Creating a New Project
To create a new JCC Express MVC project, use the official starter template:
```bash
bunx jcc-express-starter my-express-app
```
This command will create a new directory named `my-express-app` with all the necessary files and directory structure for your application.
### Project Setup
After creating your project, navigate to the project directory:
```bash
cd my-express-app
```
Then install the project dependencies:
```bash
npm install
```
### Starting the Development Server
Once dependencies are installed, you can start the development server:
```bash
bun run dev
```
Your application will be available at `http://localhost:5500` (or the port specified in your `.env` file).
---
# Configuration
JCC Express MVC uses a centralized configuration system that allows you to manage your application's settings in a clean, organized manner. All configuration files are stored in the `app/Config` directory, and sensitive values are managed through environment variables.
## Environment Configuration
For security and flexibility, JCC Express MVC uses environment files to store sensitive configuration values. Your application's `.env` file should not be committed to version control, as it may contain API keys, passwords, and other sensitive information.
### Environment File Setup
Copy the example environment file to create your own:
```bash
cp .env.example .env
```
### Environment Variables
Edit your `.env` file to configure your application. Here are the essential variables:
```env
# Application
APP_NAME="JCC Express"
APP_ENV=local
APP_KEY=your-secret-key-here
APP_DEBUG=true
APP_URL=http://localhost:5500
PORT=5500
# Database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=jcc_express
DB_USERNAME=root
DB_PASSWORD=
# Cache
CACHE_DRIVER=memory
CACHE_PREFIX=jcc_express_cache
# Session
SESSION_DRIVER=memory
SESSION_SECRET=your-session-secret
# Redis (optional)
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
```
### Accessing Environment Variables
You can access environment variables in your application using the `config` helper or by importing the config directly:
```typescript
import { config } from "@/app/Config";
const appName = config.get("APP_NAME");
const dbConnection = config.get("DB_CONNECTION");
```
## Configuration Files
All configuration files are located in the `app/Config/` directory. These files allow you to configure specific aspects of your application.
### CORS Configuration
The `cors.ts` file allows you to configure Cross-Origin Resource Sharing settings:
```typescript
// app/Config/cors.ts
export const cors = {
origin: "*", // or specify allowed origins: ["http://localhost:3000"]
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
preflightContinue: false,
optionsSuccessStatus: 204,
credentials: true, // Allow cookies
};
```
### Rate Limiting
Configure rate limiting in `app/Config/rate-limit.ts`:
```typescript
export const rateLimit = {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
};
```
### Session Configuration
Configure session settings in `app/Config/session.ts`:
```typescript
export const session = {
secret: process.env.SESSION_SECRET || "your-secret-key",
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.APP_ENV === "production",
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
};
```
---
# Global Helpers
JCC Express MVC provides several global helper functions that are available everywhere in your application without needing to import them. These helpers are automatically registered when your application boots.
## Available Global Helpers
### Application Instance
Access the application instance anywhere in your code:
```typescript
const userService = app.resolve("UserService");
const config = app.resolve("Config");
```
### Environment Variables
Get environment variable values with optional default:
```typescript
const port = env("PORT", "5500");
const dbHost = env("DB_HOST");
const appName = env("APP_NAME", "JCC Express");
```
### Event Dispatching
Dispatch events using the `emit` helper:
```typescript
import { UserRegistered } from "@/Events/UserRegistered";
// Dispatch an event
await emit(new UserRegistered(user));
```
### Queue Job Dispatching
Dispatch queue jobs using the `dispatch` helper:
```typescript
import { ProcessPodcast } from "@/Jobs/ProcessPodcast";
// Dispatch a job immediately
await dispatch(new ProcessPodcast(podcast));
// Jobs with delay are automatically handled
const job = new ProcessPodcast(podcast);
job.delay = 5000; // 5 seconds
await dispatch(job);
```
### String Utilities
Access the `Str` utility class:
```typescript
const slug = str().slug("Hello World"); // "hello-world"
const random = str().random(10); // Random string
const camel = str().camel("hello_world"); // "helloWorld"
```
### Password Hashing
Hash and verify passwords:
```typescript
// Hash a password
const hashedPassword = await bcrypt("plain-text-password");
// Verify a password
const isValid = await verifyHash("plain-text-password", hashedPassword);
```
### JWT Tokens
Sign and verify JWT tokens:
```typescript
// Sign a JWT token
const token = jwtSign({ id: 1, email: "user@example.com" });
// Verify a JWT token
try {
const payload = jwtVerify(token);
console.log(payload); // { id: 1, email: "user@example.com" }
} catch (error) {
// Token is invalid
}
```
### Root Path
Get the root path of your application:
```typescript
const storagePath = rootPath("storage/app");
const configPath = rootPath("app/Config");
const publicPath = rootPath("public");
```
## Complete Example
Here's an example showing multiple global helpers in use:
```typescript
import { UserRegistered } from "@/Events/UserRegistered";
import { SendWelcomeEmail } from "@/Jobs/SendWelcomeEmail";
// In a controller or service
async function registerUser(data: any) {
// Hash password
const hashedPassword = await bcrypt(data.password);
// Create user
const user = await User.create({
...data,
password: hashedPassword,
});
// Dispatch event
await emit(new UserRegistered(user));
// Dispatch job
await dispatch(new SendWelcomeEmail(user));
// Generate JWT token
const token = jwtSign({ id: user.id, email: user.email });
// Get storage path
const avatarPath = rootPath(`storage/app/avatars/${user.id}`);
return { user, token };
}
```
---
# Directory Structure
JCC Express MVC follows a convention-over-configuration approach with a familiar, organized directory structure. Understanding this structure will help you know where to place files and how the framework organizes code.
## Root Directory Structure
```
project-root/
āāā app/ # Application core
ā āāā Config/ # Configuration files
ā āāā Events/ # Event classes
ā āāā Http/ # HTTP layer
ā ā āāā Controllers/ # Controller classes
ā ā āāā Middlewares/ # Middleware classes
ā ā āāā Requests/ # Form request classes
ā ā āāā kernel.ts # HTTP kernel (middleware registration)
ā āāā Jobs/ # Queue job classes
ā āāā Listener/ # Event listener classes
ā āāā Models/ # Eloquent model classes
ā āāā Observer/ # Model observer classes
ā āāā Providers/ # Service provider classes
ā āāā Repository/ # Repository classes
ā āāā Services/ # Service classes
āāā bootstrap/ # Application bootstrapping
ā āāā app.ts # Application initialization
ā āāā providers.ts # Service provider registration
āāā database/ # Database files
ā āāā migrations/ # Database migration files
ā āāā seeders/ # Database seeder classes
āāā public/ # Publicly accessible files
ā āāā build/ # Compiled assets
āāā resources/ # Raw, uncompiled assets
ā āāā views/ # Template files (jsBlade)
ā āāā css/ # CSS files
ā āāā js/ # JavaScript files
āāā routes/ # Route definitions
ā āāā web.ts # Web routes
ā āāā api.ts # API routes
āāā storage/ # Storage directory
ā āāā app/ # Application files
ā āāā sessions/ # Session files
āāā tests/ # Test files
ā āāā Feature/ # Feature tests
ā āāā Unit/ # Unit tests
āāā server.ts # Application entry point
```
## The App Directory
The `app` directory contains the core code of your application. Almost all of your application's classes will be in this directory.
### Controllers
Controllers are stored in `app/Http/Controllers` and handle incoming HTTP requests. They contain the logic for processing requests and returning responses.
### Middleware
Middleware classes are stored in `app/Http/Middlewares` and provide a mechanism for filtering HTTP requests entering your application.
### Models
Eloquent models are stored in `app/Models`. Models allow you to query for data in your database tables and insert new records.
### Providers
Service providers are stored in `app/Providers` and are the central place to bootstrap your application. They bind services into the service container and register event listeners.
## The Routes Directory
The `routes` directory contains all of your route definitions. The framework ships with two route files: `web.ts` for your web routes and `api.ts` for your API routes.
## The Resources Directory
The `resources` directory contains your raw, uncompiled assets such as CSS, JavaScript, and view templates.
## The Public Directory
The `public` directory contains the `index.php` file, which is the entry point for all requests entering your application. This directory also serves as a good place to store assets such as images, fonts, and compiled CSS and JavaScript files.
---
# Lifecycle Overview
Understanding the request lifecycle of JCC Express MVC will help you better understand how the framework works and where to hook into the framework's execution flow.
JCC Express MVC uses Express's native request/response lifecycle, extending Express's `Request` and `Response` objects with additional methods through `AppRequest` and `AppResponse` interfaces. This means all standard Express middleware and methods work seamlessly with the framework.
## Request Lifecycle
Every request to your JCC Express MVC application follows a specific lifecycle path. Understanding this lifecycle is crucial for building effective applications. The framework uses Express's native HTTP handling, so requests and responses are standard Express objects with additional framework methods.
### Entry Point
The entry point for all requests to a JCC Express MVC application is the `server.ts` file. This file is very simple and serves as the starting point for loading the rest of your application:
```typescript
// server.ts
import { app } from "./bootstrap/app";
app.run();
```
### Application Bootstrap
When a request enters your application, it first goes through the application bootstrap process:
1. **Service Container**: The application instance is created and the service container is initialized
2. **Service Providers**: All service providers are registered and booted
3. **Configuration**: Application configuration is loaded
4. **Routes**: Route files are loaded and registered
### HTTP Kernel
After bootstrapping, the request is sent to the HTTP kernel (`app/Http/kernel.ts`). The kernel handles:
1. **Global Middleware**: Applies middleware that should run on every request
2. **Route Matching**: Matches the request URI to a defined route
3. **Route Middleware**: Applies middleware specific to the matched route
### Route Execution
Once a route is matched:
1. **Controller Resolution**: If the route uses a controller, the controller is instantiated via the service container
2. **Dependency Injection**: Constructor and method dependencies are automatically resolved
3. **Method Execution**: The controller method or route closure is executed
4. **Response Generation**: A response is generated and returned
### Response
The response flows back through the middleware stack (in reverse order) and is finally sent to the client.
## Service Provider Lifecycle
Service providers are the central place where your application is bootstrapped. They have two lifecycle methods:
1. **Register**: Bind services into the service container
2. **Boot**: Perform any actions after all providers are registered
Understanding this lifecycle will help you know where to place your code and how to extend the framework's functionality.
---
# Service Container
The JCC Express MVC service container is a powerful tool for managing class dependencies and performing dependency injection. Dependency injection is a method of removing hard-coded class dependencies and replacing them with injected dependencies, making your code more maintainable and testable.
## Understanding the Service Container
The service container is the central place where all of your application's services are registered and resolved. It's responsible for automatically resolving class dependencies through constructor injection.
## Dependency Injection
The service container automatically resolves dependencies by examining a class's constructor type hints. When the container sees a type-hinted dependency, it will attempt to resolve it from the container.
### Basic Dependency Injection
You can type-hint dependencies in your controllers and other classes. The service container will automatically resolve them:
```typescript
import { Inject } from "jcc-express-mvc";
import { UserService } from "@/Services/UserService";
@Inject()
export class UserController {
constructor(private userService: UserService) {}
async index() {
return this.userService.all();
}
}
```
### Binding Services
You can bind services into the container in your service providers. It's recommended to use `class.name` instead of string literals for better maintainability:
```typescript
import { ServiceProvider } from "jcc-express-mvc";
import { UserService } from "@/Services/UserService";
import { EmailService } from "@/Services/EmailService";
export class AppServiceProvider extends ServiceProvider {
register(): void {
// Bind as singleton using class.name (recommended)
this.app.singleton(UserService.name, () => {
return new UserService();
});
// Bind as instance (new instance each time) using class.name
this.app.bind(EmailService.name, () => {
return new EmailService();
});
// You can also bind directly without a factory function
this.app.singleton(UserService.name, UserService);
this.app.bind(EmailService.name, EmailService);
}
}
```
**Why use `class.name`?**
- More maintainable: If you rename the class, TypeScript will catch errors
- Less error-prone: No typos in string literals
- Better IDE support: Autocomplete and refactoring work better
- Type-safe: TypeScript can verify the class name matches
### Resolving Services
You can resolve services from the container using either the class name or `class.name`:
```typescript
import { app } from "@/bootstrap/app";
import { UserService } from "@/Services/UserService";
// Using class.name (recommended)
const userService = app.resolve(UserService.name);
// or with type annotation
const userService = app.resolve<UserService>(UserService.name);
// Using string literal (still works, but not recommended)
const userService = app.resolve("UserService");
```
## When to Use the Service Container
You typically don't need to manually interact with the service container. The framework automatically resolves dependencies through constructor injection. However, you may need to interact with the container when:
- Writing service providers
- Manually resolving services
- Binding custom services
---
# Service Providers
Service providers are the central place where all JCC Express MVC application bootstrapping takes place. Your own application, as well as all of the framework's core services, are bootstrapped via service providers.
## What are Service Providers?
Service providers are classes that bootstrap your application by binding services into the service container, registering event listeners, and performing other initialization tasks. Think of service providers as the "glue" that holds your application together.
## Writing Service Providers
All service providers extend the `ServiceProvider` class. Most service providers contain a `register` method and a `boot` method.
### The Register Method
Within the `register` method, you should only bind things into the service container. You should never attempt to register any event listeners, routes, or any other piece of functionality within the `register` method.
```typescript
import { ServiceProvider } from "jcc-express-mvc";
import { UserService } from "@/Services/UserService";
import { EmailService } from "@/Services/EmailService";
export class AppServiceProvider extends ServiceProvider {
/**
* Register any application services.
*/
register(): void {
// Bind services into the container using class.name (recommended)
this.app.singleton(UserService.name, () => {
return new UserService();
});
// Or bind directly
this.app.bind(EmailService.name, EmailService);
}
}
```
### The Boot Method
The `boot` method is called after all other service providers have been registered, meaning you have access to all other services that have been registered by the framework.
```typescript
import { ServiceProvider } from "jcc-express-mvc";
export class AppServiceProvider extends ServiceProvider {
register(): void {
// ...
}
/**
* Bootstrap any application services.
*/
boot(): void {
// Access other services here
const userService = this.app.resolve("UserService");
// Perform initialization tasks
}
}
```
## Registering Providers
All service providers are registered in the `bootstrap/providers.ts` file:
```typescript
import { AppServiceProvider } from "@/Providers/AppServiceProvider";
import { EventServiceProvider } from "@/Providers/EventServiceProvider";
import { QueueServiceProvider } from "@/Providers/QueueServiceProvider";
export const providers = [
AppServiceProvider,
EventServiceProvider,
QueueServiceProvider,
// Add your custom providers here
];
```
## Event Service Providers
For event-related functionality, extend the `EventServiceProvider` class:
```typescript
import { EventServiceProvider as ServiceProvider } from "jcc-express-mvc";
import { UserRegistered } from "@/Events/UserRegistered";
import { SendWelcomeEmail } from "@/Listeners/SendWelcomeEmail";
export class EventServiceProvider extends ServiceProvider {
protected listen: Record<any, Function[]> = {
UserRegistered: [SendWelcomeEmail],
};
protected subscribe: any[] = [];
register(): void {}
}
```
## Deferred Providers
If your provider only registers bindings in the service container, you may choose to defer its registration until one of its registered bindings is actually needed. Deferring the loading of such a provider will improve the performance of your application, since it is not loaded from the filesystem on every request.
---
# Routing
## Basic Routing
The most basic JCC Express routes accept a URI and a closure, providing a very simple and expressive method of defining routes and behavior without complicated routing configuration files:
```typescript
import { Route } from "jcc-express-mvc/Core";
Route.get("/", (req, res) => {
return res.json({ message: "Hello World" });
});
// Using Controller Array Syntax
Route.get("/user", [UserController, "index"]);
```
## Available Router Methods
The router allows you to register routes that respond to any HTTP verb:
```typescript
Route.get(uri, callback);
Route.post(uri, callback);
Route.put(uri, callback);
Route.patch(uri, callback);
Route.delete(uri, callback);
```
## Route Parameters
### Required Parameters
Sometimes you will need to capture segments of the URI within your route. For example, you may need to capture a user's ID from the URL. The framework supports both `:id` (Express-style) and `{id}` (Laravel-style) syntaxes:
```typescript
// Using :id syntax (Express-style)
Route.get("/user/:id", (req, res) => {
return res.json({ id: req.params.id });
});
// Using {id} syntax (Laravel-style) - also works
Route.get("/user/{id}", (req, res) => {
return res.json({ id: req.params.id });
});
```
Both syntaxes work identically and can be used interchangeably based on your preference.
### Route Model Binding
JCC Express automatically resolves Eloquent models defined in routes or controller actions whose type-hinted variable names match a route segment name.
```typescript
// Define a route with model binding (both :user and {user} work)
Route.get("/users/:user", [UsersController, "show"]);
// or
Route.get("/users/{user}", [UsersController, "show"]);
// Controller
import { httpContext } from "jcc-express-mvc";
import { Inject, Method } from "jcc-express-mvc";
@Inject()
class UsersController {
@Method()
async show(user: User, { res } = httpContext) {
return res.json(user);
}
}
```
#### Binding by Specific Column
You can specify which column to use for model binding by using the `{column$param}` syntax:
```typescript
// Find user by slug instead of ID
Route.get("/users/{slug$user}", [UsersController, "show"]);
// or with Express-style syntax
Route.get("/users/:slug$user", [UsersController, "show"]);
// Controller - the model will be resolved using the 'slug' column
@Inject()
class UsersController {
@Method()
async show(user: User, { res } = httpContext) {
return res.json(user); // User found by slug column
}
}
```
**Note:** The syntax `{column$param}` or `:column$param` tells the framework to find the model using the specified column instead of the default primary key.
## Route Groups
Route groups allow you to share route attributes, such as middleware, across a large number of routes without needing to define those attributes on each individual route.
### Middleware
To assign middleware to all routes within a group, you may use the `middleware` method before defining the group. Middleware are executed in the order they are listed in the array:
```typescript
Route.middleware(["auth"]).group(() => {
Route.get("/", (req, res) => {
// Uses Auth Middleware
});
Route.get("/user/profile", (req, res) => {
// Uses Auth Middleware
});
});
```
### Route Prefixes
The `prefix` method may be used to prefix each route in the group with a given URI. For example, you may want to prefix all route URIs within the group with `admin`:
```typescript
Route.prefix("admin").group(() => {
Route.get("/users", (req, res) => {
// Matches The "/admin/users" URL
});
});
```
### Route Controllers
If a group of routes all utilize the same controller, you may use the `controller` method to define the common controller for all of the routes within the group. Then, when defining the routes, you only need to provide the controller method that they invoke:
```typescript
Route.controller(OrderController).group(() => {
// Both :id and {id} syntaxes work
Route.get("/orders/:id", "show");
// or
Route.get("/orders/{id}", "show");
Route.post("/orders", "store");
});
```
---
# Controllers
## Introduction
Instead of defining all of your request handling logic as closures in your route files, you may wish to organize this behavior using "controller" classes. Controllers can group related request handling logic into a single class.
## Basic Controllers
Controllers are stored in the `app/Http/Controllers` directory.
```typescript
import { httpContext } from "jcc-express-mvc";
import { User } from "@/Models/User";
export class UserController {
/**
* Show the profile for a given user.
*/
async show({ req, res } = httpContext) {
const user = await User.find(req.params.id);
return res.json({ user });
}
}
```
## Dependency Injection & Method Injection
The framework features an improved controller architecture with method injection and elegant context handling.
### Constructor Injection
The JCC Express service container is used to resolve all controllers. As a result, you are able to type-hint any dependencies your controller may need in its constructor.
```typescript
import { Inject } from "jcc-express-mvc";
@Inject()
class UsersController {
constructor(private readonly service: UserService) {}
}
```
### Method Injection
In addition to constructor injection, you may also type-hint dependencies on your controller's methods. A common use-case for method injection is injecting the `User` model into your controller methods.
```typescript
import { Inject, Method } from "jcc-express-mvc";
@Inject()
class UsersController {
@Method()
async show(user: User, { res } = httpContext) {
return res.json(user);
}
}
```
### HTTP Context
You can destructure the HTTP context (`req`, `res`, `next`) directly in your method signature:
```typescript
@Method()
async index({ req } = httpContext) {
const { query } = req.query;
// ...
}
```
---
# Middleware
## Introduction
Middleware provide a convenient mechanism for inspecting and filtering HTTP requests entering your application. For example, JCC Express includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to the login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application.
## Defining Middleware
Middleware can be created inside the `app/Http/Middlewares` directory. Import `Request`, `Response`, and `Next` from `"jcc-express-mvc"`:
```typescript
// app/Http/Middlewares/AuthMiddleware.ts
import { Request, Response, Next } from "jcc-express-mvc";
export function AuthMiddleware(req: Request, res: Response, next: Next) {
if (!req.user) {
return res.status(401).json({ message: "Unauthorized" });
}
next();
}
```
**Note:** Always import `Request`, `Response`, and `Next` from `"jcc-express-mvc"` - these are aliases for `AppRequest`, `AppResponse`, and `AppNext` that provide full type support.
## Registering Middleware
### Global Middleware
If you want a middleware to run during every HTTP request to your application, list the middleware class in the `middleware` property of your `app/Http/kernel.ts` class.
### Assigning Middleware to Routes
If you would like to assign middleware to specific routes, you should first assign the middleware a key in your `app/Http/kernel.ts` file.
```typescript
// app/Http/kernel.ts
export class Kernel {
static middlewareAliases = {
auth: AuthMiddleware,
};
}
```
Once defined, you may use the `middleware` method to assign middleware to a route:
```typescript
Route.middleware(["auth"]).get("/profile", (req, res) => {
// ...
});
```
---
# Requests & Responses
JCC Express MVC extends Express's native `Request` and `Response` objects with additional methods through `AppRequest` and `AppResponse` interfaces. This provides a clean, fluent interface for working with HTTP requests and responses while maintaining compatibility with all Express methods.
## HTTP Requests
JCC Express MVC uses Express's `Request` object extended with additional methods via the `AppRequest` interface. All standard Express request methods are available, plus the framework's custom methods.
### Accessing The Request
In controllers, you use `httpContext` to access the request and response objects. TypeScript automatically infers the correct types:
```typescript
import { httpContext } from "jcc-express-mvc";
class UserController {
async store({ req, res } = httpContext) {
const name = req.body.name;
const email = req.body.email;
// ...
}
}
```
**Note:** In route closures (like `Route.get("/", async (req, res) => {})`), TypeScript automatically knows the types - you don't need to import or type them explicitly.
### Request Data Methods
#### Basic Input Access
```typescript
// Get all input (body + query)
const all = req.body;
// Get specific input value
const name = req.input("name");
const email = req.input("email", "default@example.com"); // With default
// Get query parameters (Express native)
const page = req.query.page;
// Get route parameters (Express native)
const userId = req.params.id;
```
#### Input Helper Methods
```typescript
// Check if input key exists
if (req.has("name")) {
// ...
}
// Check if input key exists and is not empty
if (req.filled("email")) {
// ...
}
// Get only specified keys
const data = req.only("name", "email");
// Get all except specified keys
const data = req.except("password", "password_confirmation");
// Merge new data into request
req.merge({ status: "active" });
// Replace all request data
req.replace({ name: "John", email: "john@example.com" });
```
### Authentication & User
```typescript
// Access authenticated user
const user = req.user;
// Check if user is authenticated
if (req.user) {
// User is authenticated
}
```
### Flash Messages
```typescript
// Get all flash messages
const flash = req.flash();
// Get flash messages by type
const successMessages = req.flash("success");
const errorMessages = req.flash("error");
// Set flash message
req.flash("success", "User created successfully!");
req.flash("error", ["Error 1", "Error 2"]); // Multiple messages
```
### Request Validation
```typescript
// Validate request data
await req.validate({
name: ["required", "string", "max:255"],
email: ["required", "email", "unique:users,email"],
password: ["required", "string", "min:8", "confirmed"],
});
// Get validated data
const validated = await req.validated();
```
### Request Detection Methods
```typescript
// Check if request is an API request
if (req.isApi()) {
// Request is to /api/* route
}
// Check if request is AJAX
if (req.ajax()) {
// Request is XMLHttpRequest
}
// Check if request wants JSON
if (req.wantsJson()) {
// Accept header includes JSON
}
// Check if request body is JSON
if (req.isJson()) {
// Content-Type is application/json
}
// Check if request accepts JSON
if (req.acceptsJson()) {
// Accept header prioritizes JSON
}
// Check if request expects JSON response
if (req.expectsJson()) {
// Combines isApi, ajax, wantsJson, isJson checks
}
// Check if request is Inertia request
if (req.isInertia()) {
// Request has X-Inertia header
}
```
### Headers & Cookies
```typescript
// Get header value
const authHeader = req.header("Authorization");
const userAgent = req.userAgent();
// Get cookie value
const token = req.cookie("auth_token");
// Get bearer token from Authorization header
const token = req.bearerToken();
```
### File Uploads
```typescript
// Check if file exists
if (req.hasFile("avatar")) {
// File was uploaded
}
// Get file and store it
const filePath = req.file("avatar").store("avatars");
// Access file directly (Express native)
const file = req.files?.avatar;
```
### Request Information
```typescript
// Get full URL
const fullUrl = req.fullUrl();
// Check HTTP method
if (req.isMethod("POST")) {
// ...
}
// Get request ID
const requestId = req.id;
// Access JCC session
const session = req.jccSession;
// Get previous URLs
const previousUrls = req.previsiousUrls;
```
### Complete Request Example
```typescript
// In route closures, TypeScript automatically infers types - no imports needed
Route.post("/users", async (req, res) => {
// Validate request
await req.validate({
name: ["required", "string"],
email: ["required", "email"],
});
// Get validated data
const data = await req.validated();
// Check request type
if (req.expectsJson()) {
// Return JSON response
return res.json({ message: "User created", user: data });
}
// Set flash message
req.flash("success", "User created successfully!");
// Redirect
return res.redirect("/users");
});
```
**Note:** In route closures, you don't need to import `Request` or `Response` - TypeScript automatically knows the types. Only import them if you need to use them in middleware or other contexts.
## HTTP Responses
JCC Express MVC uses Express's `Response` object extended with additional methods via the `AppResponse` interface. All standard Express response methods are available, plus the framework's custom methods.
### JSON Responses
**Note:** In route closures, TypeScript automatically infers the types for `req` and `res` - you don't need to import or type them explicitly.
```typescript
// JSON response (Express native)
return res.json({
name: "Abigail",
state: "CA",
});
// JSON with status code
return res.status(201).json({
message: "Created",
data: user,
});
```
### Redirect Responses
```typescript
// Simple redirect (Express native)
return res.redirect("/home");
// Redirect with status code
return res.redirect(303, "/home");
// Redirect back to previous URL
return res.redirectBack();
// Redirect with flash message
return res.with("User created!", "success").redirect("/users");
```
### View Responses
```typescript
// Render jsBlade view (Express native res.render)
return res.render("welcome", {
name: "John",
title: "Welcome",
});
```
### Inertia Responses
```typescript
// Render Inertia page
return res.inertia("Users/Index", {
users: users,
});
// Inertia redirect
return res.inertiaRedirect("/users", "User created!", "success");
```
### File Downloads
```typescript
// Download file (Express native)
return res.download("/path/to/file.pdf");
// Download with custom filename
return res.download("/path/to/file.pdf", "custom-name.pdf");
```
### Response Headers
```typescript
// Set header (Express native)
res.set("X-Custom-Header", "value");
// Set multiple headers
res.set({
"X-Custom-Header": "value",
"X-Another-Header": "another-value",
});
// Get header
const contentType = res.get("Content-Type");
```
### Status Codes
```typescript
// Set status code (Express native)
res.status(201);
// Chain with response
return res.status(201).json({ message: "Created" });
// Common status codes
res.status(200); // OK
res.status(201); // Created
res.status(400); // Bad Request
res.status(401); // Unauthorized
res.status(404); // Not Found
res.status(500); // Internal Server Error
```
### Flash Messages with Redirects
```typescript
// Set flash message and redirect
return res.with("User created successfully!", "success").redirect("/users");
// Different flash types
res.with("Error occurred!", "error").redirect("/users");
res.with("Warning message!", "warning").redirect("/users");
res.with("Info message!", "info").redirect("/users");
```
### Complete Response Example
```typescript
// In route closures, TypeScript automatically infers types - no imports needed
Route.post("/users", async (req, res) => {
const user = await User.create(await req.validated());
if (req.expectsJson()) {
return res.status(201).json({
message: "User created",
user: user,
});
}
return res
.with("User created successfully!", "success")
.redirect("/users");
});
```
## Request & Response Lifecycle
JCC Express MVC uses Express's native request/response lifecycle. The framework extends these objects with additional methods while maintaining full compatibility with Express middleware and methods.
### Standard Express Methods
All standard Express `Request` and `Response` methods are available:
**Request Methods:**
- `req.body` - Request body
- `req.query` - Query parameters
- `req.params` - Route parameters
- `req.headers` - Request headers
- `req.cookies` - Request cookies
- `req.get()` - Get header
- `req.is()` - Check content type
- And all other Express request methods
**Response Methods:**
- `res.json()` - JSON response
- `res.send()` - Send response
- `res.render()` - Render view
- `res.redirect()` - Redirect
- `res.status()` - Set status code
- `res.set()` - Set header
- `res.cookie()` - Set cookie
- And all other Express response methods
### Framework Extensions
The framework adds the following methods to enhance the Express objects:
**AppRequest Extensions:**
- `req.validate()` - Validate request data
- `req.validated()` - Get validated data
- `req.input()` - Get input value
- `req.has()` - Check if input exists
- `req.filled()` - Check if input is filled
- `req.only()` - Get only specified keys
- `req.except()` - Get all except specified keys
- `req.merge()` - Merge new data
- `req.replace()` - Replace request data
- `req.flash()` - Flash messages
- `req.isApi()` - Check if API request
- `req.ajax()` - Check if AJAX request
- `req.wantsJson()` - Check if wants JSON
- `req.expectsJson()` - Check if expects JSON
- `req.isInertia()` - Check if Inertia request
- `req.file()` - Get uploaded file
- `req.hasFile()` - Check if file exists
- `req.store()` - Store uploaded file
- `req.bearerToken()` - Get bearer token
- `req.userAgent()` - Get user agent
- `req.cookie()` - Get cookie
- `req.header()` - Get header
- `req.isMethod()` - Check HTTP method
- `req.fullUrl()` - Get full URL
**AppResponse Extensions:**
- `res.inertia()` - Render Inertia page
- `res.inertiaRedirect()` - Inertia redirect
- `res.redirectBack()` - Redirect to previous URL
- `res.with()` - Set flash message and chain
---
# Validation
JCC Express MVC uses the [validatorjs](https://www.npmjs.com/package/validatorjs) package for validation, with custom validation methods registered by the framework. This provides a powerful, flexible validation system that works seamlessly with Express requests.
## Introduction
JCC Express MVC provides several different approaches to validate your application's incoming data:
1. **Inline Validation** - Using `req.validate()` directly in controllers
2. **Form Request Classes** - Encapsulating validation logic in dedicated request classes
Both approaches use the same validation rules and syntax.
## Inline Validation
You can validate request data directly in your controllers using the `req.validate()` method:
```typescript
// In route closures, TypeScript automatically infers types - no imports needed
Route.post("/users", async (req, res) => {
// Validate request data
await req.validate({
name: "required|string|max:255",
email: "required|email|unique:users,email", // Can use table name or model name
password: "required|string|min:8",
});
// Get validated data
const validated = await req.validated();
// Create user with validated data
const user = await User.create(validated);
return res.json({ user });
});
```
### Validation Rules Syntax
Rules can be specified as:
- **String format**: `"required|email|min:5"`
- **Array format**: `["required", "email", "min:5"]`
```typescript
// String format (pipe-separated)
await req.validate({
name: "required|string|max:255",
email: "required|email",
});
// Array format
await req.validate({
name: ["required", "string", "max:255"],
email: ["required", "email"],
});
```
### Custom Error Messages
You can provide custom error messages:
```typescript
await req.validate(
{
name: "required|string",
email: "required|email",
},
{
"name.required": "The name field is required.",
"email.email": "That doesn't look like an email address.",
}
);
```
## Form Request Validation
For more complex validation scenarios, you may wish to create a "form request". Form requests are custom request classes that encapsulate their own validation and authorization logic.
### Creating Form Requests
To create a form request class, use the `make:request` Artisan command:
```bash
bun artisanNode make:request StoreUserRequest
```
The generated class will be placed in the `app/Http/Requests` directory.
### Writing Form Requests
Form requests are custom request classes that extend `FormRequest`. Let's look at an example form request:
```typescript
import { FormRequest } from "jcc-express-mvc/Core/FormRequest";
export class StoreUserRequest extends FormRequest {
/**
* Get the validation rules that apply to the request.
*/
async rules() {
await this.validate({
name: "required|string|max:255",
email: "required|email|unique:users,email", // Can use table name or model name
password: "required|string|min:8",
});
}
/**
* Get custom messages for validator errors.
*/
messages() {
return {
"name.required": "The name field is required.",
"email.unique": "This email is already taken.",
};
}
}
```
### Using Form Requests
Now you can type-hint the form request in your controller method. The incoming request data will be automatically validated before the controller method is called:
```typescript
import { StoreUserRequest } from "@/Http/Requests/StoreUserRequest";
import { httpContext } from "jcc-express-mvc";
class UserController {
async store(request: StoreUserRequest, { res } = httpContext) {
// The incoming request has been validated...
// You can access validated data via request.body or request.validated()
const validated = await request.validated();
const { name, email } = validated;
// Create the user...
const user = await User.create(validated);
return res.json({ user });
}
}
```
## Available Validation Rules
JCC Express MVC provides a wide variety of validation rules. The framework uses [validatorjs](https://www.npmjs.com/package/validatorjs) as the base validation library, which means all validatorjs rules are available. Additionally, the framework registers custom validation methods for database operations and other framework-specific needs.
### All Validatorjs Rules
Since JCC Express MVC uses validatorjs, all standard validatorjs rules are available. Refer to the [validatorjs documentation](https://www.npmjs.com/package/validatorjs) for the complete list of available rules. Common rules include:
- `accepted`, `active_url`, `after:date`, `after_or_equal:date`
- `alpha`, `alpha_dash`, `alpha_num`
- `array`, `before:date`, `before_or_equal:date`
- `between:min,max`, `boolean`
- `confirmed`, `date`, `date_equals:date`, `date_format:format`
- `different:field`, `digits:value`, `digits_between:min,max`
- `dimensions`, `distinct`, `email`, `exists:table,column`
- `file`, `filled`, `gt:field`, `gte:field`
- `image`, `in:value1,value2`, `in_array:field`
- `integer`, `ip`, `ipv4`, `ipv6`
- `json`, `lt:field`, `lte:field`
- `max:value`, `mimetypes`, `mimes`
- `min:value`, `not_in:value1,value2`
- `not_regex:pattern`, `nullable`, `numeric`
- `present`, `regex:pattern`, `required`
- `required_if:field,value`, `required_unless:field,value`
- `required_with:field1,field2`, `required_with_all:field1,field2`
- `required_without:field1,field2`, `required_without_all:field1,field2`
- `same:field`, `size:value`, `string`
- `timezone`, `unique:table,column`, `url`, `uuid`
### Framework Custom Rules
The framework registers the following custom validation methods that extend validatorjs:
- `unique:Model,column` - Database uniqueness validation
- `nullable` - Allows null/empty values (field is optional)
- `sometimes` - Only validates the field if it is present in the request
- `file` - Validates that a file was uploaded
- `image` - Validates that an image file was uploaded
These custom rules are automatically registered and available for use in your validation rules.
### Basic Rules
- `required` - The field must be present and not empty
### String Rules
- `string` - The field must be a string
- `min:value` - The field must have a minimum length/value
- `max:value` - The field must have a maximum length/value
- `alpha` - The field must contain only alphabetic characters
- `alphaNum` - The field must contain only alphanumeric characters
- `slug` - The field must be a valid slug
### Numeric Rules
- `numeric` or `num` - The field must be numeric
- `integer` or `int` - The field must be an integer
- `float` - The field must be a float
- `decimal` - The field must be a decimal number
### Email & URL Rules
- `email` - The field must be a valid email address
- `url` - The field must be a valid URL
### Comparison Rules
- `same:field` - The field must match another field (e.g., password confirmation)
- `confirmed` - The field must have a matching `{field}_confirmation` field
### Database Rules
- `unique:Model,column` or `unique:table,column` - The field must be unique in the database
- Example: `unique:User,email` - Checks if email is unique in User model
- Example: `unique:users,email` - Checks if email is unique in users table
- Example: `unique:users` - Checks if the field value is unique in users table (uses field name as column)
- You can use either model class name (e.g., `User`) or table name (e.g., `users`)
- If column is not specified, it defaults to the field name being validated
### Array & Object Rules
- `array` - The field must be an array
- `object` - The field must be an object
### Boolean Rules
- `boolean` or `bool` - The field must be a boolean value
### Special Format Rules
- `json` - The field must be valid JSON
- `jwt` - The field must be a valid JWT token
- `creditCard` - The field must be a valid credit card number
- `phone` - The field must be a valid phone number
- `postal:countryCode` - The field must be a valid postal code for the given country
- `mongoId` - The field must be a valid MongoDB ObjectId
### File Rules
- `file` - The field must be an uploaded file (validates file presence)
- `image` - The field must be an uploaded image file (validates image file presence)
**Note:** The framework automatically normalizes file and image fields during validation. When using `file` or `image` rules, the validator checks for file presence using `req.hasFile(field)`.
### Complete Rules Example
```typescript
await req.validate({
// Basic
name: "required|string|max:255",
// Email with uniqueness check (can use table name or model name)
email: "required|email|unique:users,email",
// Password with confirmation
password: "required|string|min:8",
password_confirmation: "required|same:password",
// Numeric
age: "required|integer|min:18|max:100",
price: "required|decimal|min:0",
// Optional fields (nullable allows field to be empty)
bio: "nullable|string|max:1000",
// Conditional validation (only validates if field is present)
phone: "sometimes|phone",
// Arrays and objects
tags: "required|array",
metadata: "nullable|object",
// Special formats
website: "nullable|url",
phone_number: "nullable|phone",
postal_code: "nullable|postal:US",
// File uploads
document: "required|file", // Required file upload
avatar: "nullable|image", // Optional image upload
resume: "sometimes|file", // Only validate if file is provided
});
```
### File Validation Example
Here's a complete example of validating file uploads:
```typescript
// In route closures, TypeScript automatically infers types - no imports needed
Route.post("/profile", async (req, res) => {
await req.validate({
name: "required|string|max:255",
avatar: "nullable|image", // Optional profile image
resume: "required|file", // Required resume file
});
const validated = await req.validated();
// Handle file uploads
if (req.hasFile("avatar")) {
const avatarPath = req.file("avatar").store("avatars");
// Save avatar path to database
}
if (req.hasFile("resume")) {
const resumePath = req.file("resume").store("resum