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
beforemiddleware.
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.