Resource-Based Routing in Alapa
Resource-based routing in Alapa provides a streamlined way to define CRUD (Create, Read, Update, Delete) operations for a resource. This approach automatically maps common actions to specific routes, reducing boilerplate code and ensuring consistency across your application.
Introduction to Resource-Based Routing
Resource-based routing is a convention-over-configuration approach that maps HTTP verbs to controller
actions for a specific resource. For example, a Book
resource might have routes for listing books,
showing a specific book, creating a new book, updating a book, and deleting a book.
Alapa simplifies this process by automatically generating these routes when you define a resourceful controller.
Default Resourceful Routes
When you define a resourceful controller, Alapa generates the following routes by default:
HTTP Verb | URL | Action | Route Name | Description |
---|---|---|---|---|
GET | /books | index | books.index | List all books |
GET | /books/create | create | books.create | Show the form to create a new book |
POST | /books | store | books.store | Store a newly created book |
GET | /books/:book | show | books.show | Show a specific book |
GET | /books/:book/edit | edit | books.edit | Show the form to edit a book |
PUT/PATCH | /books/:book | update | books.update | Update a specific book |
DELETE | /books/:book | destroy | books.destroy | Delete a specific book |
Implementing a Resourceful Controller
To implement a resourceful controller, create a class that implements the ResourcefulController
interface.
Each method in the class corresponds to the actions.
Example: BookController
import { ResourcefulController, Request, Response } from "alapa";
import { Books } from "../../models/book";
export class BooksController implements ResourcefulController {
// List all books
async index(req: Request, res: Response) {
const books = await Books.find();
res.render("books.index", { books });
}
// Show the form to create a new book (optional)
create(req: Request, res: Response) {
res.render("books.create");
}
// Store a newly created book
async store(req: Request, res: Response) {
const book = new Books();
book.title = req.body.title;
book.author = req.body.author;
await book.save();
res.navigate.back("success", "New Book Created Successfully");
}
// Show a specific book
async show(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
res.render("books.show", { book });
} else {
res.render("error.404", { message: "Book not found" });
}
}
// Show the form to edit a book (optional)
async edit(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
res.render("books.edit", { book });
}
// Update a specific book
async update(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
book.title = req.body.title;
book.author = req.body.author;
await book.save();
req.flash("success", "Book updated successfully");
} else {
req.flash("error", "Book not found");
}
res.navigate.back();
}
// Delete a specific book
async destroy(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
await book.delete();
req.flash("success", "Book deleted successfully");
} else {
req.flash("error", "Book not found");
}
res.navigate.route("books.index");
}
}
RestfulResource (or ApiResource)
For RESTful APIs, Alapa provides a specialized interface called RestfulResource
(or ApiResource
). This interface excludes methods like create
and edit
, which are typically used for rendering forms in web applications. Instead, it focuses on the core CRUD operations required for APIs.
Example: BookApiController
import { RestfulResource, Request, Response } from "alapa";
import { Books } from "../../models/book";
export class BooksController implements RestfulResource {
// List all books
async index(req: Request, res: Response) {
const books = await Books.find();
res.json(books);
}
// Store a newly created book
async store(req: Request, res: Response) {
const book = new Books();
book.title = req.body.title;
book.author = req.body.author;
await book.save();
res.status(201).json(book);
}
// Show a specific book
async show(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
res.json(book);
} else {
res.status(404).json({ message: "Book not found" });
}
}
// Update a specific book
async update(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
book.title = req.body.title;
book.author = req.body.author;
await book.save();
res.json(book);
} else {
res.status(404).json({ message: "Book not found" });
}
}
// Delete a specific book
async destroy(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
await book.delete();
res.status(204).send();
} else {
res.status(404).json({ message: "Book not found" });
}
}
}
Registering RestfulResource Routes
import { Router } from "alapa";
import { BooksController } from "./ApiController";
const bookRoutes = new Router();
bookRoutes.restResource("books", BookApiController);
// OR
bookRoutes.apiResource("books", BookApiController);
export default bookRoutes;
This will generate the following routes:
HTTP Verb | URL | Action | Route Name |
---|---|---|---|
GET | /books | index | books.index |
POST | /books | store | books.store |
GET | /books/:book | show | books.show |
PUT/PATCH | /books/:book | update | books.update |
DELETE | /books/:book | destroy | books.destroy |
Using the @Params
Decorator
The @Params
decorator is used to define dynamic segments in your routes. It allows you to specify the parameter names for resourceful routes.
Example: Using @Params
import { ResourcefulController, Request, Response, Params } from "alapa";
import { Books } from "../../models/book";
export class BookController implements ResourcefulController {
// /books/:book/:author
@Params("author") // Define the parameter name
async show(req: Request, res: Response) {
const book = await Books.findOneBy({ id: req.params.book });
if (book) {
res.render("books.show", { book });
} else {
res.navigate.back().withErrors("Book not found");
}
}
}
ResourcefulOptions Configuration
The ResourcefulOptions
interface provides a wide range of options to customize resourceful routes. Below is a detailed breakdown of these options with examples:
changeNamesWithVerbs
- Type:
boolean
- Default:
true
- Description: Whether to change route names to use the provided verb names.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
changeNamesWithVerbs: false, // Use the default route names
});
paramNames
- Type:
string
- Description: Custom parameter names for dynamic segments in the route.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
paramNames: "id", // Use `id` instead of `book` in the route
});
docPrefix
- Type:
string
- Description: Prefix added to the API documentation path.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
docPrefix: "api/v1", // Add a prefix to API documentation paths
});
createNames
- Type:
boolean
- Default:
true
- Description: Whether to create route names for create actions.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
createNames: false, // Don't create route names
});
namePrefix
- Type:
string
- Description: Prefix added to route names.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
namePrefix: "app-books", // Add a prefix to route names
});
only
- Type:
string[] | string
- Description: Specify which routes to include.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
only: ["index", "show"], // Only include index and show routes
});
except
- Type:
string[] | string
- Description: Specify which routes to exclude.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
except: ["create", "edit"], // Exclude create and edit routes
});
verb
- Type:
ResourcefulVerb
- Description: Custom verb names for resourceful routes.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
verb: {
index: "list", // Use "list" instead of "index"
show: "detail", // Use "detail" instead of "show"
},
});
middleware
- Type:
ResourcefulMiddleware
- Description: Middleware functions for resourceful routes.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
middleware: {
index: [authMiddleware], // Apply middleware to the index action
store: [logMiddleware], // Apply middleware to the store action
},
});
mergeMiddleware
- Type:
boolean
- Default:
false
- Description: Whether to merge a single route middleware with the
before
middleware.
Example:
import { BooksController } from "./ApiController";
bookRoutes.resource("books", BooksController, {
middleware: {
before: [authMiddleware], // Apply middleware before all actions
index: [logMiddleware], // Apply middleware to the index action
},
mergeMiddleware: true, // Merge middleware for index with before middleware
});
Best Practices
- Use Meaningful Resource Names: Choose resource names that clearly describe the entity (e.g.,
books
,users
). - Keep Controllers Focused: Each controller should handle a single resource.
- Leverage Middleware: Use middleware for cross-cutting concerns like authentication and logging.
- Document Your API: Use OpenAPI decorators to document your routes.
- Test Your Routes: Write unit and integration tests for your resourceful routes.