Introduction to REST API URL Design
16 minute read
URLs (Uniform Resource Locators) form the foundation of any REST API design. They serve as the primary interface between clients and your API, making them one of the most critical aspects of your API architecture. Well-designed URLs improve discoverability, usability, and the overall developer experience.
Why URL Design Matters
A thoughtful URL design offers several significant benefits:
- Intuitive Navigation: Well-structured URLs serve as a natural map of your API’s resources
- Self-Documentation: Clear URLs can reduce the learning curve for API consumers
- Consistency: Standardized URL patterns make your API more predictable
- Developer Experience: URLs are often the first thing developers see and evaluate
- SEO Benefits: For public APIs, well-designed URLs can improve search engine visibility
- Longevity: Good URL design reduces the need for breaking changes
URLs represent a contract between your API and its consumers. Once published, changing your URL structure constitutes a breaking change that impacts all clients. This makes careful upfront design essential for long-term API stability.
REST URL Design Principles
Resource-Oriented Design
REST URLs should identify resources, not actions or operations on those resources. Resources are the nouns of your API, while HTTP methods represent the verbs that act upon them. This separation is fundamental to RESTful design.
Naming, Collections and Nouns
The purpose of a URI is to uniquely address resources. The URI structure should be designed to allow intuitive navigation through your data model, making it understandable even to humans who are browsing your API for the first time.
Collections and Singular Resources
Resources in REST APIs are typically organized into collections (plural nouns) and individual resources (identified by unique IDs):
Collections: Use plural nouns to represent collections of resources
/users
- All users in the system/products
- All products in the catalog/orders
- All orders in the system
Individual Resources: Identify specific resources using unique identifiers
/users/42
- The user with ID 42/products/xyz-89
- The product with ID xyz-89/orders/2023-01-15
- The order from January 15, 2023
Consistent Resource Naming
Follow these naming conventions for clear, consistent URLs:
Use Nouns, Not Verbs
- Good:
/orders
(resource) - Avoid:
/getOrders
(action)
- Good:
Use Plural for Collections
- Good:
/products
- Avoid:
/product
- Good:
Use Kebab Case for Multi-Word Resources
- Good:
/shipping-addresses
- Avoid:
/shippingAddresses
or/shipping_addresses
- Good:
Be Consistent with Casing
- Choose one standard (typically kebab-case for URLs) and apply it consistently
- Good:
/order-items
- Avoid mixing:
/orderItems
and/shipping-addresses
Avoid Special Characters
- Good:
/reports/annual-2025
- Avoid:
/reports/annual%202025
- Good:
Example: Retrieving Task Collections and Individual Tasks
When a client makes a GET request to the tasks collection, the API should return a list of task resources:
// GET /tasks
[
{
"id": 123,
"title": "Buy flowers",
"isDone": false,
"dueDate": "2025-05-15T10:00:00Z",
"priority": "medium"
},
{
"id": 789,
"title": "Water flowers",
"isDone": false,
"dueDate": "2025-05-16T17:00:00Z",
"priority": "low"
}
// ... more tasks here
]
Each resource should include a unique identifier (id
) that can be used to address the individual resource:
// GET /tasks/123
{
"id": 123,
"title": "Buy flowers",
"isDone": false,
"dueDate": "2025-05-15T10:00:00Z",
"priority": "medium",
"description": "Purchase bouquet for anniversary",
"assignee": {
"id": 42,
"name": "John Doe"
},
"tags": ["personal", "important"]
}
Implementation Example in ASP.NET Core
// ASP.NET Core controller implementing the resource pattern
[ApiController]
[Route("api/tasks")]
public class TasksController : ControllerBase
{
private readonly ITaskRepository _taskRepository;
public TasksController(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
// GET api/tasks
[HttpGet]
public ActionResult<IEnumerable<TaskDto>> GetTasks()
{
IEnumerable<TaskDto> tasks = _taskRepository.GetAll();
return Ok(tasks);
}
// GET api/tasks/{id}
[HttpGet("{id}")]
public ActionResult<TaskDto> GetTask(int id)
{
TaskDto task = _taskRepository.GetById(id);
if (task == null)
{
return NotFound();
}
return Ok(task);
}
}
Actions are HTTP Verbs
Verbs represent actions on resources that are implemented in REST via HTTP Request Methods. Each verb has a specific semantic meaning that should be respected in your API design.
Standard HTTP Verbs for CRUD Operations
HTTP Verb | CRUD | Resource Target | Examples | Action Description | Expected Response | Common Status Codes |
---|---|---|---|---|---|---|
GET | Read | Collection or individual resource | /tasks , /tasks/123 | Retrieve a resource or collection | The requested resource(s) | 200 OK, 404 Not Found, 304 Not Modified |
POST | Create | Collection | /tasks | Add a new resource to a collection | The created resource or a reference to it | 201 Created, 400 Bad Request, 409 Conflict |
PUT | Update/Replace | Individual resource | /tasks/123 | Replace an existing resource completely | The updated resource | 200 OK, 404 Not Found, 409 Conflict |
PATCH | Update/Modify | Individual resource | /tasks/123 | Apply partial updates to a resource | The updated resource or status | 200 OK, 404 Not Found, 409 Conflict |
DELETE | Delete | Individual resource | /tasks/123 | Remove an existing resource | Empty response or deleted resource | 200 OK, 204 No Content, 404 Not Found, 409 Conflict |
Less Common but Useful HTTP Verbs
HTTP Verb | Purpose | Examples |
---|---|---|
HEAD | Retrieve headers only | Check if a resource exists or has been modified |
OPTIONS | Discover available operations | CORS preflight requests, API capabilities discovery |
Examples of HTTP Verbs in Action
Due to the different verbs, a single URL endpoint can offer different functionality based on the HTTP method used:
# Creating a new task
POST /tasks HTTP/1.1
Content-Type: application/json
{
"title": "Learn REST API design",
"dueDate": "2025-06-01T12:00:00Z",
"priority": "high"
}
# Response (201 Created)
{
"id": 456,
"title": "Learn REST API design",
"dueDate": "2025-06-01T12:00:00Z",
"priority": "high",
"isDone": false
}
# Retrieving all tasks
GET /tasks HTTP/1.1
Accept: application/json
# Updating a task completely
PUT /tasks/456 HTTP/1.1
Content-Type: application/json
{
"title": "Master REST API design",
"dueDate": "2025-06-15T12:00:00Z",
"priority": "critical",
"isDone": false
}
# Partially updating a task
PATCH /tasks/456 HTTP/1.1
Content-Type: application/json
{
"priority": "critical",
"dueDate": "2025-06-20T12:00:00Z"
}
# Deleting a task
DELETE /tasks/456 HTTP/1.1
Implementation in ASP.NET Core
[ApiController]
[Route("api/tasks")]
public class TasksController : ControllerBase
{
private readonly ITaskRepository _taskRepository;
public TasksController(ITaskRepository taskRepository)
{
_taskRepository = taskRepository;
}
// GET: api/tasks
[HttpGet]
public ActionResult<IEnumerable<TaskDto>> GetTasks()
{
IEnumerable<TaskDto> tasks = _taskRepository.GetAll();
return Ok(tasks);
}
// GET: api/tasks/{id}
[HttpGet("{id}")]
public ActionResult<TaskDto> GetTask(int id)
{
TaskDto task = _taskRepository.GetById(id);
if (task == null)
{
return NotFound();
}
return Ok(task);
}
// POST: api/tasks
[HttpPost]
public ActionResult<TaskDto> CreateTask(CreateTaskDto createTaskDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
TaskDto newTask = _taskRepository.Create(createTaskDto);
// Return 201 Created with the resource location and the created resource
return CreatedAtAction(
nameof(GetTask),
new { id = newTask.Id },
newTask
);
}
// PUT: api/tasks/{id}
[HttpPut("{id}")]
public IActionResult UpdateTask(int id, UpdateTaskDto updateTaskDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != updateTaskDto.Id)
{
return BadRequest("ID mismatch");
}
bool exists = _taskRepository.Exists(id);
if (!exists)
{
return NotFound();
}
TaskDto updatedTask = _taskRepository.Update(updateTaskDto);
return Ok(updatedTask);
}
// PATCH: api/tasks/{id}
[HttpPatch("{id}")]
public IActionResult PartialUpdateTask(int id, JsonPatchDocument<TaskDto> patchDoc)
{
TaskDto existingTask = _taskRepository.GetById(id);
if (existingTask == null)
{
return NotFound();
}
patchDoc.ApplyTo(existingTask, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
TaskDto updatedTask = _taskRepository.Update(existingTask);
return Ok(updatedTask);
}
// DELETE: api/tasks/{id}
[HttpDelete("{id}")]
public IActionResult DeleteTask(int id)
{
TaskDto existingTask = _taskRepository.GetById(id);
if (existingTask == null)
{
return NotFound();
}
_taskRepository.Delete(id);
return NoContent(); // 204 No Content is often preferred for DELETE operations
}
}
Best Practices for HTTP Methods
Respect HTTP Method Semantics: Use the correct HTTP method for each operation
Use GET for Safe Operations: GET requests should never alter state or have side effects
Use PUT for Complete Resource Updates: When replacing an entire resource
Use PATCH for Partial Updates: When updating only specific fields of a resource
Return Appropriate Status Codes: Match the status code to the operation result
Include Location Headers: For resource creation, include the URL of the new resource
Consider Idempotency: PUT, DELETE, and GET operations should be idempotent
Hierarchical Resources and Relationships
Many resources have relationships and hierarchies that should be reflected in your URL design. REST APIs should provide intuitive ways to navigate these relationships.
Resource Relationships in URLs
Resources are often related to each other in one of these ways:
Parent-Child Relationship: A parent resource contains or owns child resources
/articles/{id}/comments
- All comments belonging to a specific article/users/{id}/addresses
- All addresses belonging to a specific user
Association Relationship: Resources are related but neither owns the other
/products/{id}/related
- Products related to a specific product/users/{id}/followers
- Users who follow a specific user
Design Patterns for Resource Relationships
Consider the following guidelines when designing URLs for related resources:
Nested Resources for Ownership
/articles/123/comments
- All comments for article 123/articles/123/comments/456
- Specific comment 456 on article 123
Separate Resources with Cross-References
/articles/123
- The article resource contains comment IDs/comments/456
- Comment resources can be accessed directly
Resource Expansion
/articles/123?expand=comments
- Retrieves article with embedded comments
Example of Hierarchical Resource
For complex resources like articles with authors, comments, and tags:
// GET /articles/123
{
"id": 123,
"createdOn": "2025-01-27T10:27:00+03:00",
"modifiedOn": "2025-02-03T16:47:00+01:00",
"title": "REST is nice! Use it now in your public HTTP API!",
"text": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr...",
"textType": "PlainText",
"author": {
"id": 3105,
"name": "Benjamin Abt",
"email": "[email protected]",
"nickname": "Ben",
"twitter": {
"url": "https://twitter.com/abt_benjamin",
"username": "abt_benjamin",
"avatarUrl": "https://pbs.twimg.com/profile_images/1404387914344243203/oUO_LNAp_400x400.jpg"
}
},
"tags": [".NET", "Azure", "API", "REST"],
"comments": [
{
"id": 8758,
"text": "Lorem ipsum dolor sit amet...",
"on": "2025-01-29T16:47:00+01:00",
"by": {
"id": 789,
"name": "Batman"
}
},
{
"id": 485,
"text": "Lorem ipsum dolor sit amet...",
"on": "2025-01-30T16:47:00+01:00",
"by": {
"id": 325,
"name": "Robin"
}
}
],
"_links": {
"self": { "href": "/articles/123" },
"author": { "href": "/authors/3105" },
"comments": { "href": "/articles/123/comments" }
}
}
URL Design for Hierarchical Resources
For the article example, the following URL structure provides logical access to related resources:
# Primary resource access
GET /articles/123 # Get specific article
GET /articles # Get all articles
# Accessing related resources
GET /articles/123/author # Get author of article 123
GET /articles/123/comments # Get all comments for article 123
GET /articles/123/tags # Get all tags for article 123
# Direct access to related resources
GET /authors/3105 # Get author with ID 3105
GET /comments/485 # Get specific comment 485
Implementation in ASP.NET Core
[ApiController]
[Route("api/articles")]
public class ArticlesController : ControllerBase
{
private readonly IArticleRepository _articleRepository;
private readonly ICommentRepository _commentRepository;
public ArticlesController(
IArticleRepository articleRepository,
ICommentRepository commentRepository)
{
_articleRepository = articleRepository;
_commentRepository = commentRepository;
}
// GET: api/articles
[HttpGet]
public ActionResult<IEnumerable<ArticleDto>> GetArticles()
{
IEnumerable<ArticleDto> articles = _articleRepository.GetAll();
return Ok(articles);
}
// GET: api/articles/{id}
[HttpGet("{id}")]
public ActionResult<ArticleDto> GetArticle(int id)
{
ArticleDto article = _articleRepository.GetById(id);
if (article == null)
{
return NotFound();
}
return Ok(article);
}
// GET: api/articles/{id}/comments
[HttpGet("{id}/comments")]
public ActionResult<IEnumerable<CommentDto>> GetArticleComments(int id)
{
bool articleExists = _articleRepository.Exists(id);
if (!articleExists)
{
return NotFound("Article not found");
}
IEnumerable<CommentDto> comments = _commentRepository.GetByArticleId(id);
return Ok(comments);
}
// GET: api/articles/{articleId}/comments/{commentId}
[HttpGet("{articleId}/comments/{commentId}")]
public ActionResult<CommentDto> GetArticleComment(int articleId, int commentId)
{
bool articleExists = _articleRepository.Exists(articleId);
if (!articleExists)
{
return NotFound("Article not found");
}
CommentDto comment = _commentRepository.GetById(commentId);
if (comment == null || comment.ArticleId != articleId)
{
return NotFound("Comment not found for this article");
}
return Ok(comment);
}
// POST: api/articles/{id}/comments
[HttpPost("{id}/comments")]
public ActionResult<CommentDto> AddComment(int id, CreateCommentDto commentDto)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
bool articleExists = _articleRepository.Exists(id);
if (!articleExists)
{
return NotFound("Article not found");
}
CommentDto newComment = _commentRepository.Create(id, commentDto);
return CreatedAtAction(
nameof(GetArticleComment),
new { articleId = id, commentId = newComment.Id },
newComment
);
}
}
Best Practices for Hierarchical Resources
Consistency: Use consistent patterns for expressing relationships
Limit Nesting: Avoid deep nesting (more than 2-3 levels) as it becomes unwieldy
- Good:
/articles/123/comments/456
- Avoid:
/users/42/articles/123/comments/456/replies/789
- Good:
Multiple Access Paths: Provide direct access to resources when appropriate
/articles/123/comments/456
- Nested access/comments/456
- Direct access
Use Links: Include hypermedia links in responses to guide clients to related resources
Consider Query Parameters for Filtering Related Resources
/comments?articleId=123
as an alternative to/articles/123/comments
Balance between Embedding and Linking: Decide whether to embed related resources or link to them based on typical usage patterns
Query Parameters for Filtering, Sorting, and Pagination
REST APIs often need to handle large collections of resources, requiring mechanisms for clients to filter results, control sorting order, and paginate through large result sets.
Filtering
Filtering allows clients to retrieve subsets of a resource collection by specifying criteria. While REST doesn’t standardize filtering syntax, several common patterns have emerged:
Basic Filtering by Resource Properties
Use query parameters that match property names:
# Filter by single property
GET /articles?author=3105
GET /products?category=electronics
GET /tasks?isDone=false
# Filter by multiple properties (AND logic)
GET /articles?author=3105&isPublished=true
GET /products?category=electronics&inStock=true
Advanced Filtering
For more complex filtering needs, consider a structured filtering syntax:
# Range filtering
GET /products?price[min]=100&price[max]=500
# Array value filtering
GET /products?tags=electronics&tags=sale
# Search/contains filtering
GET /articles?title[contains]=REST
Implementation in ASP.NET Core
[ApiController]
[Route("api/articles")]
public class ArticlesController : ControllerBase
{
private readonly IArticleRepository _repository;
public ArticlesController(IArticleRepository repository)
{
_repository = repository;
}
[HttpGet]
public ActionResult<IEnumerable<ArticleDto>> GetArticles(
[FromQuery] int? authorId = null,
[FromQuery] bool? isPublished = null,
[FromQuery] string titleContains = null,
[FromQuery] string[] tags = null)
{
// Create a filter specification
ArticleFilterSpecification filter = new ArticleFilterSpecification
{
AuthorId = authorId,
IsPublished = isPublished,
TitleContains = titleContains,
Tags = tags
};
// Apply filtering
IEnumerable<ArticleDto> articles = _repository.GetFiltered(filter);
return Ok(articles);
}
}
Sorting
Sorting allows clients to specify the order of results. Common conventions include:
# Sort by a single field (ascending by default)
GET /articles?sort=title
# Explicit sort direction
GET /articles?sort=title:asc
GET /products?sort=price:desc
# Multiple sort criteria (applied in order)
GET /articles?sort=author:asc,publishDate:desc
Implementation in ASP.NET Core
[ApiController]
[Route("api/articles")]
public class ArticlesController : ControllerBase
{
private readonly IArticleRepository _repository;
public ArticlesController(IArticleRepository repository)
{
_repository = repository;
}
[HttpGet]
public ActionResult<IEnumerable<ArticleDto>> GetArticles(
[FromQuery] string sort = null)
{
// Parse and apply sorting
List<SortCriteria> sortCriteria = ParseSortParameter(sort);
IEnumerable<ArticleDto> articles = _repository.GetSorted(sortCriteria);
return Ok(articles);
}
private List<SortCriteria> ParseSortParameter(string sort)
{
List<SortCriteria> criteria = new List<SortCriteria>();
if (string.IsNullOrEmpty(sort))
{
return criteria;
}
string[] sortComponents = sort.Split(',');
foreach (string component in sortComponents)
{
string[] parts = component.Split(':');
string property = parts[0];
bool ascending = parts.Length == 1 || parts[1].ToLower() == "asc";
criteria.Add(new SortCriteria
{
PropertyName = property,
Ascending = ascending
});
}
return criteria;
}
}
Pagination
Pagination is essential for APIs that return large collections of resources. Two common approaches are:
Offset-Based Pagination
Uses skip
(or offset
) and limit
(or take
) parameters:
# Return the first 10 items
GET /articles?limit=10
# Skip the first 20 items and return the next 10
GET /articles?skip=20&limit=10
Cursor-Based Pagination
Uses a pointer to the last item to determine the next page:
# First page
GET /articles?limit=10
# Get next page (using the last item's ID as a cursor)
GET /articles?after=abc123&limit=10
# Get previous page
GET /articles?before=def456&limit=10
Pagination Metadata
Return pagination metadata to help clients navigate through pages:
{
"data": [
// array of resources...
],
"metadata": {
"totalCount": 738,
"pageSize": 10,
"currentPage": 3,
"totalPages": 74,
"hasNext": true,
"hasPrevious": true
},
"_links": {
"self": { "href": "/articles?page=3&pageSize=10" },
"first": { "href": "/articles?page=1&pageSize=10" },
"prev": { "href": "/articles?page=2&pageSize=10" },
"next": { "href": "/articles?page=4&pageSize=10" },
"last": { "href": "/articles?page=74&pageSize=10" }
}
}
Implementation in ASP.NET Core
[ApiController]
[Route("api/articles")]
public class ArticlesController : ControllerBase
{
private readonly IArticleRepository _repository;
public ArticlesController(IArticleRepository repository)
{
_repository = repository;
}
[HttpGet]
public ActionResult<PagedResponse<ArticleDto>> GetArticles(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
// Apply validation and limits
pageSize = Math.Min(pageSize, 100); // Maximum page size
page = Math.Max(1, page); // Minimum page number
// Get paginated results
PagedResult<ArticleDto> result = _repository.GetPaged(page, pageSize);
// Create response with metadata and HATEOAS links
PagedResponse<ArticleDto> response = new PagedResponse<ArticleDto>
{
Data = result.Items,
Metadata = new PaginationMetadata
{
TotalCount = result.TotalCount,
PageSize = pageSize,
CurrentPage = page,
TotalPages = (int)Math.Ceiling(result.TotalCount / (double)pageSize),
HasNext = page < (int)Math.Ceiling(result.TotalCount / (double)pageSize),
HasPrevious = page > 1
}
};
// Add HATEOAS links
response.AddLink("self", Url.Action(nameof(GetArticles), new { page, pageSize }));
if (response.Metadata.HasPrevious)
{
response.AddLink("prev", Url.Action(nameof(GetArticles), new { page = page - 1, pageSize }));
}
if (response.Metadata.HasNext)
{
response.AddLink("next", Url.Action(nameof(GetArticles), new { page = page + 1, pageSize }));
}
response.AddLink("first", Url.Action(nameof(GetArticles), new { page = 1, pageSize }));
response.AddLink("last", Url.Action(nameof(GetArticles), new { page = response.Metadata.TotalPages, pageSize }));
return Ok(response);
}
}
Best Practices for Query Parameters
- Be Conservative with Defaults: Apply sensible default limits (e.g., 20-50 items per page)
- Enforce Maximum Limits: To prevent excessive resource usage
- Validate and Sanitize Input: Protect against malformed or malicious query parameters
- Document Query Parameters: Clearly document all available parameters
- Consistent Naming: Use consistent parameter naming across your API
- Consider Response Formats: Include metadata to help clients process results
Security Considerations for URLs
URL design has important security implications that should be carefully considered.
Query Parameters and Sensitive Data
IMPORTANT: Query parameters in URLs are not secure for sensitive data, even with HTTPS:
- Exposure in Logs: URLs are commonly stored in server logs, browser history, and proxy logs
- Cached by Browsers: URLs might be cached in browser history
- Visible in Referer Headers: URLs are sent in Referer headers to third-party sites
- Bookmarks and Sharing: URLs can be easily shared or bookmarked
Solution: Never include sensitive data such as:
- Authentication tokens or credentials
- Personal identifiable information (PII)
- Financial information
- Any confidential business data
Instead, use:
- HTTP Headers: For API keys, tokens, and other authentication data
- Request Body: For sensitive data in POST/PUT/PATCH requests
- HTTP Authorization Header: For credentials and tokens
Implementation Example (ASP.NET Core)
// AVOID THIS - Security risk
[HttpGet("user-data")]
public IActionResult GetUserDataInsecure([FromQuery] string token, [FromQuery] int userId)
{
// Token in URL is insecure
return Ok();
}
// DO THIS INSTEAD
[HttpGet("user-data")]
public IActionResult GetUserDataSecure([FromHeader(Name = "Authorization")] string token)
{
// Token from header is more secure
// User ID from claims rather than URL parameter
int userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Ok();
}
API Versioning
API versioning is essential for evolving your API while maintaining backward compatibility. Several strategies exist for versioning REST APIs:
URL Path Versioning
Include the version in the URL path:
/api/v1/articles
/api/v2/articles
Advantages:
- Explicit and visible
- Easy to route to different codebases
- Simple to understand
Disadvantages:
- Changes the resource URL when version changes
- Can lead to code duplication
Query Parameter Versioning
Pass the version as a query parameter:
/api/articles?version=1
/api/articles?version=2
Advantages:
- Preserves the same resource URL
- Easy to implement
Disadvantages:
- Less visible in documentation
- Can be overlooked by developers
Header-Based Versioning
Use a custom header to specify the API version:
GET /api/articles
Accept-Version: 1
GET /api/articles
Accept-Version: 2
Advantages:
- Keeps URLs clean and consistent
- Follows HTTP’s content negotiation philosophy
Disadvantages:
- Less visible, harder to test manually
- Requires custom header support
Media Type Versioning
Embed version in the Accept header using content type:
GET /api/articles
Accept: application/vnd.company.api.v1+json
GET /api/articles
Accept: application/vnd.company.api.v2+json
Advantages:
- Leverages existing HTTP content negotiation
- Sophisticated approach for complex APIs
Disadvantages:
- More complex to implement
- Less intuitive for API consumers
ASP.NET Core Implementation
// Program.cs setup for URL path versioning
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
builder.Services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// Controller example
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/articles")]
public class ArticlesV1Controller : ControllerBase
{
// V1 implementation
}
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/articles")]
public class ArticlesV2Controller : ControllerBase
{
// V2 implementation with new features
}
Best Practice for API Versioning
- Version from the Start: Even your first API should be versioned
- Choose One Strategy: Pick the versioning strategy that best fits your needs and use it consistently
- Major Versions Only: Only increment versions for breaking changes
- Support Multiple Versions: Plan to support at least one previous version
- Document Changes: Clearly document differences between versions
- Deprecation Policy: Establish a clear deprecation policy with timelines
URL Design Best Practices Summary
- Resource-Oriented: Use nouns to represent resources
- Consistent Conventions: Apply consistent naming and formatting
- Hierarchical Structure: Reflect resource relationships in URL paths
- Proper HTTP Methods: Use appropriate HTTP methods for operations
- Query Parameters: Use for filtering, sorting, and pagination
- Security-Conscious: Never expose sensitive data in URLs
- Version Appropriately: Choose a versioning strategy and apply consistently
- Limit Path Depth: Aim for URLs with 2-3 path levels for readability
- Use Plural Nouns: Represent collections with plural nouns
- Descriptive IDs: Consider using slugs or UUIDs when appropriate
Following these principles will help you create intuitive, consistent, and maintainable REST API URLs that provide a solid foundation for your API design.