jcc-express-starter
v1.3.0
Published
express mvc
Maintainers
Keywords
Readme
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/
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.
- Install Bun (if not already installed):
curl -fsSL https://bun.sh/install | bash- Verify Bun installation:
bun --versionCreating a New Project
To create a new JCC Express MVC project, use the official starter template:
bunx jcc-express-starter my-express-appThis 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:
cd my-express-appThen install the project dependencies:
npm installStarting the Development Server
Once dependencies are installed, you can start the development server:
bun run devYour 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:
cp .env.example .envEnvironment Variables
Edit your .env file to configure your application. Here are the essential variables:
# 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=6379Accessing Environment Variables
You can access environment variables in your application using the config helper or by importing the config directly:
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:
// 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:
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:
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:
const userService = app.resolve("UserService");
const config = app.resolve("Config");Environment Variables
Get environment variable values with optional default:
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:
import { UserRegistered } from "@/Events/UserRegistered";
// Dispatch an event
await emit(new UserRegistered(user));Queue Job Dispatching
Dispatch queue jobs using the dispatch helper:
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:
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:
// 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:
// Sign a JWT token
const token = jwtSign({ id: 1, email: "[email protected]" });
// Verify a JWT token
try {
const payload = jwtVerify(token);
console.log(payload); // { id: 1, email: "[email protected]" }
} catch (error) {
// Token is invalid
}Root Path
Get the root path of your application:
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:
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 pointThe 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:
// 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:
- Service Container: The application instance is created and the service container is initialized
- Service Providers: All service providers are registered and booted
- Configuration: Application configuration is loaded
- 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:
- Global Middleware: Applies middleware that should run on every request
- Route Matching: Matches the request URI to a defined route
- Route Middleware: Applies middleware specific to the matched route
Route Execution
Once a route is matched:
- Controller Resolution: If the route uses a controller, the controller is instantiated via the service container
- Dependency Injection: Constructor and method dependencies are automatically resolved
- Method Execution: The controller method or route closure is executed
- 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:
- Register: Bind services into the service container
- 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:
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:
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:
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.
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.
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:
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:
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:
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:
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:
// 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.
// 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:
// 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:
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:
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:
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.
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.
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.
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:
@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":
// 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.
// 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:
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:
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
// Get all input (body + query)
const all = req.body;
// Get specific input value
const name = req.input("name");
const email = req.input("email", "[email protected]"); // 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
// 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: "[email protected]" });Authentication & User
// Access authenticated user
const user = req.user;
// Check if user is authenticated
if (req.user) {
// User is authenticated
}Flash Messages
// 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 messagesRequest Validation
// 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
// 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
// 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
// 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
// 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
// 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.
// 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
// 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
// Render jsBlade view (Express native res.render)
return res.render("welcome", {
name: "John",
title: "Welcome",
});Inertia Responses
// Render Inertia page
return res.inertia("Users/Index", {
users: users,
});
// Inertia redirect
return res.inertiaRedirect("/users", "User created!", "success");File Downloads
// 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
// 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
// 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 ErrorFlash Messages with Redirects
// 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
// 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 bodyreq.query- Query parametersreq.params- Route parametersreq.headers- Request headersreq.cookies- Request cookiesreq.get()- Get headerreq.is()- Check content type- And all other Express request methods
Response Methods:
res.json()- JSON responseres.send()- Send responseres.render()- Render viewres.redirect()- Redirectres.status()- Set status coderes.set()- Set headerres.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 datareq.validated()- Get validated datareq.input()- Get input valuereq.has()- Check if input existsreq.filled()- Check if input is filledreq.only()- Get only specified keysreq.except()- Get all except specified keysreq.merge()- Merge new datareq.replace()- Replace request datareq.flash()- Flash messagesreq.isApi()- Check if API requestreq.ajax()- Check if AJAX requestreq.wantsJson()- Check if wants JSONreq.expectsJson()- Check if expects JSONreq.isInertia()- Check if Inertia requestreq.file()- Get uploaded filereq.hasFile()- Check if file existsreq.store()- Store uploaded filereq.bearerToken()- Get bearer tokenreq.userAgent()- Get user agentreq.cookie()- Get cookiereq.header()- Get headerreq.isMethod()- Check HTTP methodreq.fullUrl()- Get full URL
AppResponse Extensions:
res.inertia()- Render Inertia pageres.inertiaRedirect()- Inertia redirectres.redirectBack()- Redirect to previous URLres.with()- Set flash message and chain
Validation
JCC Express MVC uses the 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:
- Inline Validation - Using
req.validate()directly in controllers - 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:
// 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"]
// 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:
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:
bun artisanNode make:request StoreUserRequestThe 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:
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:
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 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 for the complete list of available rules. Common rules include:
accepted,active_url,after:date,after_or_equal:datealpha,alpha_dash,alpha_numarray,before:date,before_or_equal:datebetween:min,max,booleanconfirmed,date,date_equals:date,date_format:formatdifferent:field,digits:value,digits_between:min,maxdimensions,distinct,email,exists:table,columnfile,filled,gt:field,gte:fieldimage,in:value1,value2,in_array:fieldinteger,ip,ipv4,ipv6json,lt:field,lte:fieldmax:value,mimetypes,mimesmin:value,not_in:value1,value2not_regex:pattern,nullable,numericpresent,regex:pattern,requiredrequired_if:field,value,required_unless:field,valuerequired_with:field1,field2,required_with_all:field1,field2required_without:field1,field2,required_without_all:field1,field2same:field,size:value,stringtimezone,unique:table,column,url,uuid
Framework Custom Rules
The framework registers the following custom validation methods that extend validatorjs:
unique:Model,column- Database uniqueness validationnullable- Allows null/empty values (field is optional)sometimes- Only validates the field if it is present in the requestfile- Validates that a file was uploadedimage- 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 stringmin:value- The field must have a minimum length/valuemax:value- The field must have a maximum length/valuealpha- The field must contain only alphabetic charactersalphaNum- The field must contain only alphanumeric charactersslug- The field must be a valid slug
Numeric Rules
numericornum- The field must be numericintegerorint- The field must be an integerfloat- The field must be a floatdecimal- The field must be a decimal number
Email & URL Rules
email- The field must be a valid email addressurl- 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}_confirmationfield
Database Rules
unique:Model,columnorunique: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
- Example:
Array & Object Rules
array- The field must be an arrayobject- The field must be an object
Boolean Rules
booleanorbool- The field must be a boolean value
Special Format Rules
json- The field must be valid JSONjwt- The field must be a valid JWT tokencreditCard- The field must be a valid credit card numberphone- The field must be a valid phone numberpostal:countryCode- The field must be a valid postal code for the given countrymongoId- 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
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:
// 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("resumes");
// Save resume path to database
}
return res.json({ message: "Profile updated" });
});Custom Validation Messages
You can customize the error messages used by the validator:
// Inline validation
await req.validate(
{
email: "required|email",
password: "required|min:8",
},
{
"email.required": "We need your email address!",
"email.email": "That doesn't look like an email address.",
"password.min": "Password must be at least 8 characters.",
}
);
// In FormRequest
export class StoreUserRequest extends FormRequest {
async rules() {
await this.validate({
email: "required|email",
password: "required|min:8",
});
}
messages() {
return {
"email.required": "We need your email address!",
"email.email": "That doesn't look like an email address.",
"password.min": "Password must be at least 8 characters.",
};
}
}Custom Validation Methods
The framework registers custom validation methods that extend validatorjs. These are automatically available and handle framework-specific validation needs.
Custom Rules
The framework provides the following custom validation rules:
unique:Model,column or unique:table,column
Validates that a field value is unique in the database.
await req.validate({
// Using model class name
email: "required|email|unique:User,email",
// Using table name
username: "required|string|unique:users,username",
// Column optional - uses field name as column
email: "required|email|unique:users", // Checks users.email
username: "required|string|unique:users", // Checks users.username
});- Model/Table: Can be either the model class name (e.g.,
User) or the table name (e.g.,users) - column: The database column to check (optional, defaults to the field name being validated)
nullable
Allows the field to be null, undefined, or empty. When combined with other rules, those rules only apply if the field has a value.
await req.validate({
bio: "nullable|string|max:1000", // bio is optional, but if provided, must be string and max 1000 chars
});Normalization: If the field is not present in the request, it's normalized to an empty string.
sometimes
Only validates the field if it is present in the request. Useful for partial updates.
await req.validate({
name: "sometimes|string|max:255", // Only validates if name is provided
});Normalization: If the field is not present and sometimes is used, the field is skipped during validation.
file
Validates that a file was uploaded for the field.
await req.validate({
document: "required|file",
});Normalization: The field is normalized to a boolean (true if file exists, false otherwise) using req.hasFile(field).
image
Validates that an image file was uploaded for the field.
await req.validate({
avatar: "required|image",
thumbnail: "nullable|image", // Optional image
});Normalization: The field is normalized to a boolean (true if image file exists, false otherwise) using req.hasFile(field).
Request Body Normalization
The framework automatically normalizes the request body before validation to handle special cases:
sometimes: Fields with this rule are skipped if not presentnullable: Fields with this rule are set to empty string if not presentfile: Fields are normalized to boolean based onreq.hasFile(field)image: Fields are normalized to boolean based onreq.hasFile(field)
This normalization ensures consistent validation behavior across different request types.
Validation Error Handling
When validation fails, a ValidationException is thrown. The framework automatically:
- Flashes old input to the session (accessible via
flash.old) - Flashes validation errors to the session (accessible via
flash.validation_error) - For web requests: Errors are available in flash messages
- For API requests: Returns JSON error response with 422 status code
Accessing Validation Errors in Views
<!-- In jsBlade templates -->
@if(flash.validation_error)
<div class="errors">
@foreach(flash.validation_error as field => errors)
<div class="error">
<strong>{{ field }}:</strong>
@if(Array.isArray(errors))
{{ errors[0] }}
@else
{{ errors }}
@endif
</div>
@endforeach
</div>
@endif
<!-- Access old input -->
<input type="text" name="email" value="{{ flash.old.email }}" />Handling Validation Errors in Controllers
By default, you don't need try-catch blocks. The framework automatically handles validation errors:
// In route closures, TypeScript automatically infers types - no imports needed
Route.post("/users", async (req, res) => {
// No try-catch needed - framework handles errors automatically
await req.validate({
email: "required|email",
password: "required|min:8",
});
// Validation passed - this code only runs if validation succeeds
const user = await User.create(await req.validated());
return res.json({ user });
});The framework automatically:
- Returns a 422 JSON response for API requests with validation errors
- Redirects back with flash errors for web requests
Only use try-catch if you need custom error handling:
import { ValidationException } from "jcc-express-mvc/lib/Error/ValidationException-v2";
Route.post("/users", async (req, res) => {
try {
await req.validate({
email: "required|email",
password: "required|min:8",
});
const user = await User.create(await req.validated());
return res.json({ user });
} catch (error) {
// When try-catch is defined, you must manually handle errors
if (error instanceof ValidationException) {
// Custom error handling logic here
if (req.expectsJson()) {
return res.status(422).json({
message: "Validation failed",
errors: error.errors,
});
}
return res.redirectBack();
}
throw error; // Re-throw other errors
}
});Note: If you define try-catch in your controller, the framework won't automatically handle validation errors - you must handle them manually.
Error Response Format
When validation fails for API requests, the response format is:
{
"message": "Validation failed",
"errors": {
"email": ["The email field is required."],
"password": ["The password must be at least 8 characters."]
}
}Getting Validated Data
After successful validation, you can access the validated data:
// Using req.validated()
await req.validate({ name: "required|string" });
const validated = await req.validated();
// In FormRequest
const validated = await request.validated();Database Getting Started
JCC Express MVC makes interacting with databases extremely simple across a variety of database backends using either raw SQL, the fluent query builder, or the Eloquent ORM. Currently, JCC Express MVC supports MySQL, PostgreSQL, and SQLite.
Introduction
Most web applications interact with a database. JCC Express MVC makes connecting to databases and running queries extremely simple across a variety of supported databases using either raw SQL, a fluent query builder, or the Eloquent ORM.
Configuration
The database configuration for your application is located in your .env file and app/Config/ directory. You may define all of your database connections in these configuration files, as well as specify which connection should be used by default.
Environment Configuration
Configure your database connection in the .env file:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=jcc_express
DB_USERNAME=root
DB_PASSWORD=Supported Databases
JCC Express MVC supports the following database systems:
- MySQL 5.7+ / MariaDB 10.3+
- PostgreSQL 10.0+
- SQLite 3.8.8+
Running Raw SQL Queries
Once you have configured your database connection, you may run queries using the DB facade. The DB facade provides methods for each type of query: select, update, delete, insert, and statement.
import { DB } from "jcc-eloquent/lib/DB";
const users = await DB.select("SELECT * FROM users WHERE active = ?", [1]);Using Multiple Database Connections
When using multiple connections, you may access each connection via the DB facade's connection method:
const users = await DB.connection("mysql").table("users").get();Query Builder
The database query builder provides a convenient, fluent interface to creating and running database queries. It can be used to perform most database operations in your application and works on all supported database systems.
Introduction
The JCC Express MVC query builder provides a convenient, fluent interface to creating and running database queries. It can be used to perform most database operations in your application and works on all supported database systems.
Retrieving Results
Retrieving All Rows From A Table
You may use the table method on the DB facade to begin a query. The table method returns a fluent query builder instance for the given table, allowing you to chain more constraints onto the query and then finally retrieve the results of the query using the get method:
import { DB } from "jcc-eloquent/lib/DB";
const users = await DB.table("users").get();Retrieving A Single Row / Column From A Table
If you just need to retrieve a single row from a database table, you may use the first method:
const user = await DB.table("users").where("name", "John").first();If you don't even need an entire row, you may extract a single value from a record using the value method:
const email = await DB.table("users").where("name", "John").value("email");Chunking Results
If you need to work with thousands of database records, consider using the chunk method. This method retrieves a small chunk of results at a time and feeds each chunk into a closure for processing:
await DB.table("users").orderBy("id").chunk(100, (users) => {
for (const user of users) {
// Process each chunk of 100 users
}
});Select Statements
Specifying A Select Clause
You may not always want to select all columns from a database table. Using the select method, you can specify a custom select clause for the query:
const users = await DB.table("users")
.select("name", "email as user_email")
.get();Where Clauses
Where Clauses
You may use the query builder's where method to add "where" clauses to the query. The most basic call to where requires three arguments: the column, an operator, and the value:
const users = await DB.table("users")
.where("votes", "=", 100)
.get();For convenience, if you want to verify that a column is equal to a given value, you may pass the value directly as the second argument to the where method:
const users = await DB.table("users").where("votes", 100).get();Or Where Clauses
When chaining together calls to the query builder's where method, the "where" clauses will be joined together using the AND operator. However, you may use the orWhere method to join a clause to the query using the OR operator:
const users = await DB.table("users")
.where("votes", ">", 100)
.orWhere("name", "John")
.get();Inserts
The query builder also provides an insert method that may be used to insert records into the database table. The insert method accepts an object or array of objects:
await DB.table("users").insert({
email: "[email protected]",
votes: 0,
});
// Insert multiple records
await DB.table("users").insert([
{ email: "[email protected]", votes: 0 },
{ email: "[email protected]", votes: 0 },
]);Auto-Incrementing IDs
If the table has an auto-incrementing id, use the insertGetId method to insert a record and then retrieve the ID:
const id = await DB.table("users").insertGetId({
email: "[email protected]",
votes: 0,
});Updates
In addition to inserting records into the database, the query builder can also update existing records using the update method. The update method, like the insert method, accepts an object containing the columns and values which should be updated:
await DB.table("users")
.where("id", 1)
.update({ votes: 1 });Deletes
The query builder may also be used to delete records from the table via the delete method:
await DB.table("users").where("votes", "<", 100).delete();Migrations
Migrations are like version control for your database, allowing your team to define and share the application's database schema definition.
Generating Migrations
To create a migration, use the make:migration Artisan command:
bun artisanNode make:migration create_users_tableMigration Structure
A migration class contains two methods: up and down. The up method is used to add new tables, columns, or indexes to your database, while the down method should reverse the operations performed by the up method.
import { Schema } from "jcc-eloquent";
Schema.create("users", (table) => {
table.id();
table.string("name");
table.string("email").unique();
table.timestamps();
});Running Migrations
To run your outstanding migrations, execute the migrate Artisan command:
bun artisanNode migrateSchema Builder Methods
The Schema builder provides a fluent interface for defining database tables and columns. All methods are chainable.
Schema Methods
import { Schema } from "jcc-eloquent";
// Create a table
Schema.create("users", (table) => {
// Define columns
});
// Modify an existing table
Schema.table("users", (table) => {
// Add/modify columns
});
// Drop a table
Schema.dropTable("users");
// Drop table (without IF EXISTS)
Schema.drop("users");
// Rename a table
Schema.rename("old_users", "new_users");
// Check if table exists
const exists = await Schema.hasTable("users");
// Check if column exists
const hasColumn = await Schema.hasColumn("users", "email");
// Get column listing
const columns = await Schema.getColumnListing("users");Column Types
String Types
// String (VARCHAR)
table.string("name");
table.string("name", 100); // With length
// Char
table.char("code", 5);
// Text types
table.text("description");
table.mediumText("content");
table.longText("article");
table.tinyText("excerpt");Integer Types
// Integer
table.integer("age");
// Big integer
table.bigInteger("user_id");
// Small integer
table.smallInteger("status");
// Medium integer
table.mediumInteger("views");
// Tiny integer
table.tinyInteger("flag", 4);
// Unsigned integers
table.unsignedInteger("count");
table.unsignedBigInteger("id");
table.unsignedSmallInteger("status");
table.unsignedMediumInteger("views");
table.unsignedTinyInteger("flag");
// Auto-incrementing
table.increments("id"); // INT UNSIGNED AUTO_INCREMENT
table.bigIncrements("id"); // BIGINT UNSIGNED AUTO_IN