REST API
forge's REST API framework is inspired by Django REST Framework. Instead of writing HTTP handlers for every endpoint, you use ViewSets and get CRUD operations, pagination, and filtering with consistent defaults.
Why use it?β
Because writing API endpoints is repetitive:
- Automatic CRUD - Get all the endpoints without writing them
- Pagination - Built in, works out of the box
- Filtering - Filter by any field via query params
- Serialization - Control what fields get exposed
- Authentication - Token, JWT, sessionβpick what you need
- Permissions - Flexible permission system
Overviewβ
The REST API is built on the ViewSet pattern, providing:
- Full CRUD operations (Create, Read, Update, Delete)
- Automatic pagination
- Filtering and ordering
- Serialization
- Error handling
Basic Usageβ
1. Define a Serializerβ
Create a serializer for your model:
package models
import (
"github.com/forgego/forge/api"
)
// UserSerializer serializes User model
type UserSerializer struct {
*api.BaseSerializer
}
func NewUserSerializer() api.Serializer {
return &UserSerializer{
BaseSerializer: api.NewBaseSerializer(),
}
}
func (s *UserSerializer) Fields() []string {
return []string{"id", "username", "email", "is_active"}
}
func (s *UserSerializer) ReadOnlyFields() []string {
return []string{"id", "date_joined"}
}
2. Create a ViewSetβ
package api
import (
"myapp/models"
"github.com/forgego/forge/api"
)
func RegisterUserViewSet(router *api.Router) {
viewset := api.NewBaseViewSet(
func() api.Serializer {
return models.NewUserSerializer()
},
models.UserObjects.Filter(), // QuerySet
&models.User{}, // Model instance
)
router.Register("users", viewset)
}
3. Register Routesβ
In your main.go:
import (
"github.com/forgego/forge/api"
"github.com/forgego/forge/server"
)
func main() {
// ... setup code ...
// Create API router
apiRouter := api.NewRouter("/api/v1")
// Register viewsets
api.RegisterUserViewSet(apiRouter)
api.RegisterPostViewSet(apiRouter)
// Register on HTTP router
server.RegisterRoutes(func(router *server.Router) {
apiRouter.RegisterRoutes(router)
})
}
API Endpointsβ
Once registered, your ViewSet automatically provides these endpoints:
List (GET /api/v1/users/)β
Returns paginated list of users.
Query Parameters:
page- Page number (default: 1)page_size- Items per page (default: 20)search- Search queryordering- Order by field (use-for descending)is_active- Filter by fieldusername__contains- Field lookup filters
Response:
{
"count": 100,
"next": "http://example.com/api/v1/users/?page=2",
"previous": null,
"results": [
{
"id": 1,
"username": "john",
"email": "john@example.com",
"is_active": true
}
]
}
Detail (GET /api/v1/users/{id}/)β
Returns a single user.
Response:
{
"id": 1,
"username": "john",
"email": "john@example.com",
"is_active": true
}
Create (POST /api/v1/users/)β
Creates a new user.
Request:
{
"username": "jane",
"email": "jane@example.com",
"is_active": true
}
Response:
{
"id": 2,
"username": "jane",
"email": "jane@example.com",
"is_active": true
}
Update (PUT /api/v1/users/{id}/)β
Updates a user (full update).
Request:
{
"username": "jane_updated",
"email": "jane@example.com",
"is_active": true
}
Partial Update (PATCH /api/v1/users/{id}/)β
Partially updates a user.
Request:
{
"is_active": false
}
Delete (DELETE /api/v1/users/{id}/)β
Deletes a user.
Response: 204 No Content
Paginationβ
Pagination is automatic. Use query parameters:
GET /api/v1/users/?page=2&page_size=50
Response Format:
{
"count": 100,
"next": "http://example.com/api/v1/users/?page=3&page_size=50",
"previous": "http://example.com/api/v1/users/?page=1&page_size=50",
"results": [...]
}
Filteringβ
Filter by field values:
GET /api/v1/users/?is_active=true
GET /api/v1/users/?username__contains=john
GET /api/v1/users/?date_joined__gte=2024-01-01
Filter Lookupsβ
exact- Exact match (default)iexact- Case-insensitive exact matchcontains- Contains substringicontains- Case-insensitive containsstartswith- Starts withendswith- Ends withgt- Greater thangte- Greater than or equallt- Less thanlte- Less than or equalin- Value in listisnull- Is nullrange- Range filter
Orderingβ
Use the ordering parameter:
GET /api/v1/users/?ordering=-date_joined,username
Use - prefix for descending order.
Searchβ
Full-text search via search parameter:
GET /api/v1/users/?search=john
Searches across fields specified in the serializer's SearchFields().
Custom Serializersβ
Custom Field Serializationβ
type UserSerializer struct {
*api.BaseSerializer
}
func (s *UserSerializer) SerializeField(field string, value interface{}) interface{} {
switch field {
case "password":
return nil // Never serialize password
case "date_joined":
return value.(time.Time).Format(time.RFC3339)
default:
return value
}
}
Nested Serializationβ
type PostSerializer struct {
*api.BaseSerializer
}
func (s *PostSerializer) Fields() []string {
return []string{"id", "title", "content", "author", "created_at"}
}
func (s *PostSerializer) NestedFields() map[string]api.Serializer {
return map[string]api.Serializer{
"author": NewUserSerializer(),
}
}
Custom Viewsetsβ
Create custom viewsets for complex logic:
type PostViewSet struct {
*api.BaseViewSet
}
func NewPostViewSet() *PostViewSet {
return &PostViewSet{
BaseViewSet: api.NewBaseViewSet(
NewPostSerializer,
PostObjects.Filter(),
&Post{},
),
}
}
func (vs *PostViewSet) Create(w http.ResponseWriter, r *http.Request) {
// your custom logic here
}
func (vs *PostViewSet) List(w http.ResponseWriter, r *http.Request) {
// your custom filtering logic here
}
Permissionsβ
Add permission checks:
viewset := api.NewBaseViewSet(...)
viewset.SetPermissions(
api.RequireAuthenticated(),
api.RequirePermission("can_view_user"),
)
Authenticationβ
Add authentication to your API:
apiRouter := api.NewRouter("/api/v1")
apiRouter.Use(auth.RequireAuth())
Error Handlingβ
API errors are returned in a consistent format:
{
"error": "Validation failed",
"details": {
"username": ["This field is required."],
"email": ["Enter a valid email address."]
}
}
Best Practicesβ
- Use Serializers - Always use serializers to control API output
- Paginate Lists - Always paginate list endpoints
- Filter Appropriately - Add filters for commonly queried fields
- Validate Input - Use model validation in serializers
- Handle Errors - Return consistent error responses
- Use HTTP Status Codes - Use appropriate status codes (200, 201, 400, 404, 500)
Examplesβ
Complete API Setupβ
package main
import (
"github.com/forgego/forge/api"
"github.com/forgego/forge/server"
"myapp/api"
)
func main() {
// ... your setup code ...
apiRouter := api.NewRouter("/api/v1")
api.RegisterUserViewSet(apiRouter)
api.RegisterPostViewSet(apiRouter)
api.RegisterCategoryViewSet(apiRouter)
server.RegisterRoutes(func(router *server.Router) {
apiRouter.RegisterRoutes(router)
})
}
Next Stepsβ
- Queries Guide - Learn about QuerySet filtering
- Security Guide - Secure your API
- Advanced Topics - Extend the API system