This document covers advanced Restify features and functions that provide additional control and customization options for your REST API.
The restify.Ready() function allows you to register callback functions that will be executed when Restify has finished initializing and is ready to handle requests. This is useful for performing setup tasks that depend on Restify being fully configured.
func Ready(fn func())- Performing database seeding after models are registered
- Setting up additional routes that depend on Restify resources
- Initializing external services that integrate with your API
- Running post-initialization validation or setup
func (app App) Register() error {
// Register models first
db.UseModel(User{}, Product{}, Order{})
// Register a callback to run when Restify is ready
restify.Ready(func() {
fmt.Println("Restify is ready and all endpoints are registered!")
// Perform any post-initialization tasks here
seedDatabase()
setupMetrics()
})
return nil
}
func seedDatabase() {
// Check if database needs seeding
var count int64
db.Model(&User{}).Count(&count)
if count == 0 {
// Create default admin user
adminUser := User{
Username: "admin",
Password: "admin123",
IsAdmin: true,
Name: "Administrator",
Email: "admin@example.com",
}
db.Create(&adminUser)
fmt.Println("Default admin user created")
}
}
func setupMetrics() {
// Initialize monitoring or analytics
fmt.Println("Metrics and monitoring initialized")
}func (app App) Register() error {
// You can register multiple Ready callbacks
restify.Ready(func() {
fmt.Println("First callback: Setting up logging")
setupLogging()
})
restify.Ready(func() {
fmt.Println("Second callback: Initializing cache")
initializeCache()
})
restify.Ready(func() {
fmt.Println("Third callback: Starting background jobs")
startBackgroundJobs()
})
return nil
}func (app App) Register() error {
restify.Ready(func() {
// Initialize external service connections
initializeRedis()
setupElasticsearch()
configureMessageQueue()
// Register custom middleware that depends on these services
registerCustomMiddleware()
})
return nil
}
func initializeRedis() {
// Redis connection setup
fmt.Println("Redis connection established")
}
func setupElasticsearch() {
// Elasticsearch setup for search functionality
fmt.Println("Elasticsearch configured")
}
func configureMessageQueue() {
// Message queue setup for async processing
fmt.Println("Message queue configured")
}
func registerCustomMiddleware() {
// Add custom middleware that uses the initialized services
evo.Use(func(c *fiber.Ctx) error {
// Custom middleware logic
return c.Next()
})
}Restify provides built-in debugging capabilities that help you understand how your API endpoints are processing requests and generating SQL queries.
Add the debug=restify query parameter to any Restify endpoint to enable debug mode for that request:
# Debug a specific request
curl --location 'http://localhost:8080/admin/rest/user/all?debug=restify'
# Debug with filters
curl --location 'http://localhost:8080/admin/rest/user/all?debug=restify&name[contains]=john'
# Debug pagination
curl --location 'http://localhost:8080/admin/rest/user/paginate?debug=restify&page=1&size=10'When debug mode is enabled, you'll see detailed information in your application logs:
- Generated SQL Queries: The exact SQL queries being executed
- Query Parameters: The values being bound to the SQL queries
- Execution Time: How long each query takes to execute
- Applied Conditions: Any conditions added through Context.SetCondition
- Override Values: Any data overrides applied through Context.Override
-- Debug output in console
[2024-01-15 10:30:45] [info] [database] SELECT * FROM `user` WHERE `user`.`deleted_at` IS NULL AND ((`name` LIKE '%john%')) ORDER BY `user`.`user_id` LIMIT 10
[2024-01-15 10:30:45] [info] [database] Query took: 2.5ms
[2024-01-15 10:30:45] [info] [restify] Applied conditions: name LIKE %john%
[2024-01-15 10:30:45] [info] [restify] Total records found: 3You can also enable debug mode programmatically in your hooks or permission handlers:
func (user *User) RestPermission(permissions restify.Permissions, context *restify.Context) bool {
// Enable debug mode for this specific request
if context.Request.Query("admin_debug").String() == "true" {
context.GetDBO().Debug()
}
return true
}Restify automatically handles language headers for internationalization support. The language information is passed to the database context and can be used for localized queries.
Restify checks for language information in the following order:
- HTTP Header:
language: en-US - Cookie:
l10n-language=en-US
The language information is automatically set in the database context and can be accessed in your custom queries:
func (product *Product) OnAfterGet(context *restify.Context) error {
// Get the language from the database context
dbo := context.GetDBO()
// Use language-specific queries if needed
if lang, ok := dbo.Get("lang"); ok {
fmt.Printf("Request language: %s\n", lang)
// Load localized content based on language
if lang == "es" {
// Load Spanish content
} else if lang == "fr" {
// Load French content
}
}
return nil
}# Request with language header
curl --location 'http://localhost:8080/admin/rest/product/all' \
--header 'language: es-ES'
# Request with language cookie
curl --location 'http://localhost:8080/admin/rest/product/all' \
--cookie 'l10n-language=fr-FR'Restify provides access to the underlying database connection through the Context object, allowing you to perform custom database operations while maintaining the same transaction and context.
func (order *Order) OnBeforeCreate(context *restify.Context) error {
// Get the database connection with applied conditions
dbo := context.GetDBO()
// Perform custom database operations
var productCount int64
dbo.Model(&Product{}).Where("product_id = ?", order.ProductID).Count(&productCount)
if productCount == 0 {
context.AddValidationErrors(fmt.Errorf("product does not exist"))
return fmt.Errorf("validation error")
}
return nil
}The database connection obtained through GetDBO() automatically participates in Restify's transaction management:
func (order *Order) OnAfterCreate(context *restify.Context) error {
dbo := context.GetDBO()
// This operation will be part of the same transaction
// If it fails, the entire operation will be rolled back
err := dbo.Model(&Product{}).
Where("product_id = ?", order.ProductID).
Update("stock_quantity", gorm.Expr("stock_quantity - ?", order.Quantity)).Error
if err != nil {
return fmt.Errorf("failed to update product stock: %w", err)
}
return nil
}When you don't need all fields, use the fields parameter to reduce data transfer:
# Only load specific fields
curl --location 'http://localhost:8080/admin/rest/user/all?fields=user_id,username,email'Be selective about which associations you load:
# Load specific associations only
curl --location 'http://localhost:8080/admin/rest/user/1?associations=Orders'
# Avoid loading all associations unless necessary
curl --location 'http://localhost:8080/admin/rest/user/1?associations=*' # Use sparinglyAlways use pagination for endpoints that might return large amounts of data:
# Use pagination instead of /all for large datasets
curl --location 'http://localhost:8080/admin/rest/user/paginate?page=1&size=50'Use database indexes for fields commonly used in conditions:
func (user *User) RestPermission(permissions restify.Permissions, context *restify.Context) bool {
// Use indexed fields for conditions when possible
context.SetCondition("tenant_id", "=", currentTenant.ID) // Make sure tenant_id is indexed
context.SetCondition("status", "=", "active") // Make sure status is indexed
return true
}Restify allows you to add custom endpoints with custom features beyond the default CRUD operations. This is particularly useful when you need specialized business logic, custom filtering, or endpoints that don't follow the standard REST patterns.
You can add custom endpoints using restify.Ready() combined with resource.SetAction(). This approach allows you to:
- Create custom business logic endpoints
- Implement specialized filtering and permissions
- Add endpoints with custom URL patterns
- Integrate with external services or complex workflows
// Get a resource for a specific model
func GetResource(model interface{}) (*Resource, error)
// Add a custom endpoint to a resource
func (resource *Resource) SetAction(endpoint *Endpoint)
// Endpoint structure for custom endpoints
type Endpoint struct {
Name string // Unique name for the endpoint
Method string // HTTP method (GET, POST, PUT, PATCH, DELETE)
AbsoluteURI string // Custom URL path
Handler func(*Context) *Error // Custom handler function
Filterable bool // Whether the endpoint supports filtering
PKUrl bool // Whether the endpoint uses primary key in URL
Description string // Description for documentation
}Here's a comprehensive example showing how to add custom endpoints for a Point of Sale system:
func (app App) Register() error {
// Register models first
db.UseModel(models.Order{})
// Add custom endpoints when Restify is ready
restify.Ready(func() {
resource, err := restify.GetResource(models.Order{})
if err != nil {
log.Fatal(err)
}
// Custom endpoint for current user's orders
resource.SetAction(&restify.Endpoint{
Name: "GetSelfOrders",
Method: restify.MethodGET,
AbsoluteURI: "/api/v1/pos/self/orders",
Handler: controller.getSelfOrdersHandler,
Filterable: true,
Description: "get order list of the current user",
})
// Custom endpoint for ship orders
resource.SetAction(&restify.Endpoint{
Name: "GetShipOrders",
Method: restify.MethodGET,
AbsoluteURI: "/api/v1/pos/ship/orders",
Handler: controller.getShipOrdersHandler,
Filterable: true,
Description: "get order list of the current ship",
})
// Custom endpoint for store orders
resource.SetAction(&restify.Endpoint{
Name: "GetStoreOrders",
Method: restify.MethodGET,
AbsoluteURI: "/api/v1/pos/store/orders",
Handler: controller.getStoreOrdersHandler,
Filterable: true,
Description: "get order list of the sales channel",
})
// Custom endpoint for single order with primary key
resource.SetAction(&restify.Endpoint{
Name: "GetSingleOrder",
Method: restify.MethodGET,
PKUrl: true,
AbsoluteURI: "/api/v1/pos/order",
Handler: controller.getOrderHandler,
Description: "get single order by id",
})
})
return nil
}Each custom endpoint requires a handler function that implements your business logic:
type Controller struct{}
// Handler for user's own orders
func (c Controller) getSelfOrdersHandler(context *restify.Context) *restify.Error {
// Check authentication
if context.Request.User().Anonymous() {
return &restifyUnauthorized
}
// Validate API key
if context.Request.Header("APIKEY") != settings.Get("APP.API_KEY").String() {
return &restifyInvalidAPIKey
}
// Get current user
var user = context.Request.User().Interface().(*auth.User)
if !user.HasPermission(roles.POSSelfReport) {
return &restifyInsufficientPermission
}
// Apply custom filtering
var nodeID = context.Request.Header("X-NODE-ID")
context.ApplyFilter(func(context *restify.Context, db *gorm.DB) *gorm.DB {
return db.Debug().Where("node_id = ? AND cashier_uuid = ?", nodeID, user.UUID())
})
// Use built-in pagination handler
return (restify.Handler{}).Paginate(context)
}
// Handler for ship orders
func (c Controller) getShipOrdersHandler(context *restify.Context) *restify.Error {
if context.Request.User().Anonymous() {
return &restifyUnauthorized
}
if context.Request.Header("APIKEY") != settings.Get("APP.API_KEY").String() {
return &restifyInvalidAPIKey
}
var user = context.Request.User().Interface().(*auth.User)
if !user.HasPermission(roles.POSSalesReport) {
return &restifyInsufficientPermission
}
var nodeID = context.Request.Header("X-NODE-ID")
context.ApplyFilter(func(context *restify.Context, db *gorm.DB) *gorm.DB {
return db.Debug().Where("node_id = ?", nodeID)
})
return (restify.Handler{}).Paginate(context)
}
// Handler for store orders with complex filtering
func (c Controller) getStoreOrdersHandler(context *restify.Context) *restify.Error {
if context.Request.User().Anonymous() {
return &restifyUnauthorized
}
if context.Request.Header("APIKEY") != settings.Get("APP.API_KEY").String() {
return &restifyInvalidAPIKey
}
var user = context.Request.User().Interface().(*auth.User)
// Validate required user fields
if user.SalesChannelID == nil {
return &restifySalesChannelIDError
}
if user.CompanyID == nil {
return &restifyCompanyIDError
}
if !user.HasPermission(roles.POSStoreOrders) {
return &restifyInsufficientPermission
}
// Apply complex filtering with subquery
context.ApplyFilter(func(context *restify.Context, db *gorm.DB) *gorm.DB {
return db.Debug().Where("company_id = ? AND order_id IN (SELECT order_id FROM order_item WHERE sales_channel_id = ? )", *user.CompanyID, user.SalesChannelID)
})
return (restify.Handler{}).Paginate(context)
}
// Handler for single order retrieval
func (c Controller) getOrderHandler(context *restify.Context) *restify.Error {
if context.Request.User().Anonymous() {
return &restifyUnauthorized
}
if context.Request.Header("APIKEY") != settings.Get("APP.API_KEY").String() {
return &restifyInvalidAPIKey
}
var user = context.Request.User().Interface().(*auth.User)
if !user.HasPermission(roles.POSLogin) {
return &restifyInsufficientPermission
}
// Use built-in Get handler
return (restify.Handler{}).Get(context)
}- Ensures custom endpoints are added after Restify initialization
- Guarantees that all resources are available when adding custom actions
- Allows access to fully configured Restify resources
- Retrieves the Restify resource for a specific model
- Returns an error if the model is not registered with Restify
- Provides access to the resource's configuration and endpoints
- Name: Unique identifier for the endpoint
- Method: HTTP method (use restify.MethodGET, restify.MethodPOST, etc.)
- AbsoluteURI: Custom URL path for the endpoint
- Handler: Your custom handler function
- Filterable: Enables query parameter filtering
- PKUrl: Adds primary key parameter to the URL
- Description: Used for API documentation and Postman collections
- Receive
*restify.Contextparameter - Return
*restify.Errorfor error handling - Use
context.ApplyFilter()for custom database filtering - Leverage built-in handlers like
Paginate()andGet()when appropriate
func (c Controller) getAdvancedOrdersHandler(context *restify.Context) *restify.Error {
// Apply multiple filters
context.ApplyFilter(func(context *restify.Context, db *gorm.DB) *gorm.DB {
query := db.Debug()
// Join with related tables
query = query.Joins("LEFT JOIN users ON orders.user_id = users.user_id")
query = query.Joins("LEFT JOIN products ON orders.product_id = products.product_id")
// Apply conditional filtering
if region := context.Request.Query("region").String(); region != "" {
query = query.Where("users.region = ?", region)
}
if category := context.Request.Query("category").String(); category != "" {
query = query.Where("products.category = ?", category)
}
return query
})
return (restify.Handler{}).Paginate(context)
}func (c Controller) getOrderSummaryHandler(context *restify.Context) *restify.Error {
// Get filtered data
var orders []models.Order
dbo := context.GetDBO()
if err := dbo.Find(&orders).Error; err != nil {
return &restify.Error{
Message: "Failed to fetch orders",
Code: http.StatusInternalServerError,
}
}
// Create custom response
summary := map[string]interface{}{
"total_orders": len(orders),
"total_amount": calculateTotalAmount(orders),
"orders": orders,
}
// Return custom response
context.Response.JSON(summary)
return nil
}Define custom error types for consistent error responses:
var (
restifyUnauthorized = restify.Error{
Message: "Authentication required",
Code: http.StatusUnauthorized,
}
restifyInvalidAPIKey = restify.Error{
Message: "Invalid API key",
Code: http.StatusForbidden,
}
restifyInsufficientPermission = restify.Error{
Message: "Insufficient permissions",
Code: http.StatusForbidden,
}
restifySalesChannelIDError = restify.Error{
Message: "Sales channel ID is required",
Code: http.StatusBadRequest,
}
restifyCompanyIDError = restify.Error{
Message: "Company ID is required",
Code: http.StatusBadRequest,
}
)Once your custom endpoints are configured, you can test them:
# Test self orders endpoint
curl --location 'http://localhost:8080/api/v1/pos/self/orders' \
--header 'APIKEY: your-api-key' \
--header 'X-NODE-ID: node123' \
--header 'Authorization: Bearer your-token'
# Test ship orders with filtering
curl --location 'http://localhost:8080/api/v1/pos/ship/orders?status[eq]=pending' \
--header 'APIKEY: your-api-key' \
--header 'X-NODE-ID: node123'
# Test single order retrieval
curl --location 'http://localhost:8080/api/v1/pos/order/123' \
--header 'APIKEY: your-api-key'func (user *User) ValidateCreate(context *restify.Context) error {
// Validate email format
if !isValidEmail(user.Email) {
context.AddValidationErrors(fmt.Errorf("invalid email format"))
}
// Validate password strength
if !isStrongPassword(user.Password) {
context.AddValidationErrors(fmt.Errorf("password does not meet security requirements"))
}
return nil
}func (document *Document) RestPermission(permissions restify.Permissions, context *restify.Context) bool {
user := getCurrentUser(context)
// Always enforce tenant isolation
context.SetCondition("tenant_id", "=", user.TenantID)
// Enforce ownership for non-admin users
if !user.IsAdmin {
context.SetCondition("user_id", "=", user.UserID)
}
return true
}func (user *User) OnAfterGet(context *restify.Context) error {
// Remove sensitive information before sending response
user.Password = ""
user.InternalNotes = ""
return nil
}Always use HTTPS in production environments and consider implementing additional security headers:
func (app App) Register() error {
// Add security middleware
evo.Use(func(c *fiber.Ctx) error {
c.Set("X-Content-Type-Options", "nosniff")
c.Set("X-Frame-Options", "DENY")
c.Set("X-XSS-Protection", "1; mode=block")
return c.Next()
})
return nil
}