agentleFS
jkanindajkaninda/okapi-skills/route_definition/SKILL.md

route_definition

Skill3 starsChanged 3 months agoMITView on GitHub

Okapi's RouteDefinition is a declarative alternative to imperative route registration (.Get(), .Post(), etc.). Instead of chaining method calls, you describe each route as a struct and register it with app.Register(). This keeps route metadata — method, path, handler, documentation, middleware — in one place, making routes easier to read, move between files, and generate programmatically. Imperative (traditional): Declarative (RouteDefinition): When Group is nil the route is registered on the root instance; when the group's Okapi reference is unset it is…

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    When Group is nil the route is registered on the root instance.

    line 67
  2. Distinctive:

    Use struct fields for common metadata — they're cleaner and more readable:

    line 73
  3. Very specific:

    Use .WithTags() on groups so routes inherit tags automatically — no need to repeat Tags on every route.

    line 89

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Okapi Route Definition Skills

Okapi's RouteDefinition is a declarative alternative to imperative route registration (.Get(), .Post(), etc.). Instead of chaining method calls, you describe each route as a struct and register it with app.Register(). This keeps route metadata — method, path, handler, documentation, middleware — in one place, making routes easier to read, move between files, and generate programmatically.

RouteDefinition Struct

FieldTypeDescription
MethodstringHTTP method (http.MethodGet, http.MethodPost, etc.)
PathstringRoute path relative to its group (e.g. "/books/{id:int}")
Handlerokapi.HandlerFuncHandler function — use okapi.H() for generic input binding
Group*okapi.GroupGroup this route belongs to (prefix, middleware, tags)
OperationIdstringOpenAPI operation ID
SummarystringShort description for OpenAPI docs
DescriptionstringDetailed description for OpenAPI docs
Tags[]stringOpenAPI tags (overrides group tags if set)
RequestanyRequest body schema for OpenAPI docs
ResponseanySuccess response schema (defaults to 200)
Security[]map[string][]stringOpenAPI security requirements
Options[]okapi.RouteOptionAdditional route options (error responses, path params, etc.)
Middlewares[]okapi.MiddlewarePer-route middleware

Declarative vs Imperative

Imperative (traditional):

group.Post("/books", okapi.H(handler.Create),
    okapi.DocTags("Books"),
    okapi.DocSummary("Create a book"),
    okapi.Request(&CreateBookRequest{}),
    okapi.DocResponse(201, &BookResponse{}),
    okapi.DocResponse(409, &ErrorResponse{}),
)

Declarative (RouteDefinition):

app.Register(okapi.RouteDefinition{
    Method:  http.MethodPost,
    Path:    "/books",
    Handler: okapi.H(handler.Create),
    Group:   bookGroup,
    Summary: "Create a book",
    Request: &CreateBookRequest{},
    Options: []okapi.RouteOption{
        okapi.DocResponse(201, &BookResponse{}),
        okapi.DocResponse(409, &ErrorResponse{}),
    },
})

Registration

// On the app — single or variadic
app.Register(routeDef)
app.Register(routeDefs...)

// Package-level equivalent (takes a slice)
okapi.RegisterRoutes(app, routeDefs)

// On a group — assigns the group to every route that doesn't set one
api := app.Group("/api")
api.Register(routeDefs...)

When Group is nil the route is registered on the root instance; when the group's Okapi reference is unset it is filled in automatically. Supported methods are GET, POST, PUT, DELETE, PATCH, HEAD, and OPTIONS — an unsupported method panics.

Middleware comes from Middlewares on the definition, or from Use() on the app/group.

Using Struct Fields vs Options

Use struct fields for common metadata — they're cleaner and more readable:

Use struct fieldUse Options for
Summary, Descriptionokapi.DocPathParam(...) — path parameters
Tagsokapi.DocResponse(201, ...) — non-200 statuses
Requestokapi.DocResponse(204, nil) — no content
Response (200 only)okapi.DocResponse(404, &ErrorResponse{}) — errors
Securityokapi.DocHide() — hide from docs
Middlewaresokapi.DocQueryParam(...) — query parameters
OperationIdokapi.DocHeader(...) — header parameters

okapi.DocErrorResponse(status, v) is deprecated — use okapi.DocResponse(status, v).

Group-Level Tags with .WithTags()

Use .WithTags() on groups so routes inherit tags automatically — no need to repeat Tags on every route. Only set Tags on a route when it needs to override the group default.

// All routes in this group inherit the "Books" tag
bookGroup := app.Group("/api/v1/books", authMiddleware).WithTags([]string{"Books"})
bookGroup.WithBearerAuth()

app.Register(
    // Inherits "Books" tag from group
    okapi.RouteDefinition{
        Method:   http.MethodGet,
        Path:     "",
        Handler:  bookService.List,
        Group:    bookGroup,
        Summary:  "List books",
        Response: &BooksResponse{},
    },
    // Overrides group tag
    okapi.RouteDefinition{
        Method:  http.MethodGet,
        Path:    "/categories",
        Handler: categoryService.List,
        Group:   bookGroup,
        Tags:    []string{"Categories"},
        Summary: "List categories",
    },
)

Organizing Routes in a Large Project

For large projects with many routes, use a Router struct and split route definitions across multiple files by domain. Each file contains methods that return []okapi.RouteDefinition.

File structure:

internal/routes/
├── routes.go          # Router struct, InitRoutes, registerRoutes orchestrator
├── auth_routes.go     # Auth & public API routes
├── user_routes.go     # User-scoped routes
└── admin_routes.go    # Admin routes

Router struct (in routes.go):

type Router struct {
    app *okapi.Okapi
    cfg *config.Config
    v1  *okapi.Group

    // Auth middleware
    jwtAuth      okapi.JWTAuth
    jwtAdminAuth okapi.JWTAuth

    // Handlers
    userHandler  *handlers.UserHandler
    bookHandler  *handlers.BookHandler
    adminHandler *handlers.AdminHandler
}

func InitRoutes(app *okapi.Okapi, db *gorm.DB, cfg *config.Config) {
    // Initialize repositories, services, handlers...

    r := &Router{
        app:          app,
        cfg:          cfg,
        v1:           app.Group("/api/v1"),
        jwtAuth:      middlewares.JWTAuth(cfg),
        jwtAdminAuth: middlewares.JWTAdminAuth(cfg),
        userHandler:  handlers.NewUserHandler(userRepo),
        bookHandler:  handlers.NewBookHandler(bookRepo),
        adminHandler: handlers.NewAdminHandler(db),
    }
    r.registerRoutes()
}

func (r *Router) registerRoutes() {
    r.app.Use(okapi.RequestID())

    r.app.Register(r.authRoutes()...)
    r.app.Register(r.userRoutes()...)
    r.app.Register(r.adminRoutes()...)

    // Special registrations that don't fit RouteDefinition
    r.app.Static("/assets", "public/assets")
    r.app.Web("/", "./web") // SPA index fallback — register after every API route
}

Domain-specific route files (e.g. auth_routes.go):

func (r *Router) authRoutes() []okapi.RouteDefinition {
    authGroup := r.v1.Group("/auth").WithTags([]string{"Auth"})

    return []okapi.RouteDefinition{
        {
            Method:   http.MethodPost,
            Path:     "/login",
            Handler:  okapi.H(r.userHandler.Login),
            Group:    authGroup,
            Summary:  "Login",
            Request:  &LoginRequest{},
            Response: &AuthResponse{},
        },
    }
}

Key principles:

  • One Router struct holds all handlers, middleware, and config
  • Each file returns []okapi.RouteDefinition from methods on Router
  • Groups are created inside each method with .WithTags() to avoid tag repetition
  • registerRoutes() orchestrates all registration via app.Register()
  • Conditional routes (dev mode, optional features) use if guards in registerRoutes()
  • Special registrations (static files, NoRoute, global middleware) stay imperative

Common Patterns

CRUD resource — a typical set of routes for a resource:

func (r *Router) bookRoutes() []okapi.RouteDefinition {
    g := r.v1.Group("/books", r.jwtAuth.Middleware).WithTags([]string{"Books"})
    g.WithBearerAuth()

    return []okapi.RouteDefinition{
        {Method: http.MethodGet, Path: "", Handler: okapi.H(r.bookHandler.List), Group: g,
            Summary: "List books", Request: &ListRequest{}, Response: &PageableResponse[Book]{}},
        {Method: http.MethodGet, Path: "/{id:int}", Handler: okapi.H(r.bookHandler.Get), Group: g,
            Summary: "Get book", Response: &Response[Book]{},
            Options: []okapi.RouteOption{okapi.DocPathParam("id", "integer", "Book ID"), okapi.DocResponse(404, &ErrorResponse{})}},
        {Method: http.MethodPost, Path: "", Handler: okapi.H(r.bookHandler.Create), Group: g,
            Summary: "Create book", Request: &CreateBookRequest{},
            Options: []okapi.RouteOption{okapi.DocResponse(201, &Response[Book]{}), okapi.DocResponse(409, &ErrorResponse{})}},
        {Method: http.MethodPut, Path: "/{id:int}", Handler: okapi.H(r.bookHandler.Update), Group: g,
            Summary: "Update book", Request: &UpdateBookRequest{}, Response: &Response[Book]{},
            Options: []okapi.RouteOption{okapi.DocPathParam("id", "integer", "Book ID"), okapi.DocResponse(404, &ErrorResponse{})}},
        {Method: http.MethodDelete, Path: "/{id:int}", Handler: okapi.H(r.bookHandler.Delete), Group: g,
            Summary: "Delete book",
            Options: []okapi.RouteOption{okapi.DocPathParam("id", "integer", "Book ID"), okapi.DocResponse(204, nil), okapi.DocResponse(404, &ErrorResponse{})}},
    }
}

Conditional routes — only registered when a feature is enabled:

func (r *Router) registerRoutes() {
    r.app.Register(r.publicRoutes()...)
    r.app.Register(r.userRoutes()...)

    if r.cfg.DevMode {
        r.app.Register(r.devRoutes()...)
    }
}

Optional handler — append to the slice when a dependency is available:

func (r *Router) adminRoutes() []okapi.RouteDefinition {
    g := r.v1.Group("/admin", r.jwtAdminAuth.Middleware).WithTags([]string{"Admin"})
    g.WithBearerAuth()

    routes := []okapi.RouteDefinition{
        // ... standard admin routes
    }

    if r.cronHandler != nil {
        routes = append(routes, okapi.RouteDefinition{
            Method:  http.MethodGet,
            Path:    "/jobs",
            Handler: r.cronHandler.List,
            Group:   g,
            Summary: "List scheduled jobs",
            Response: &Response[[]JobStatus]{},
        })
    }

    return routes
}

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.