In our last session, we designed the "front door" to our SaaS: the tenant management and authentication services. We established a secure, "account-first" registration flow and defined how users will authenticate using JWTs that contain their user ID, tenant ID, and permissions. This self-contained token is the key to our stateless, multi-tenant architecture.
Today, we move from the entryway into the main rooms of our application. Our objective is to create the low-level design for the project management service, defining the API contracts and data flows for projects, tasks, and milestones. This is where we apply the authentication and RBAC framework we've designed to the core features of our construction management system. We will be translating our high-level requirements into a precise, developer-ready API specification.
From Data Model to API Resources
Before defining endpoints, it's crucial to ground our API design in the underlying data model. A well-designed RESTful API acts as an intuitive and logical interface to the application's core entities. It shouldn't be an arbitrary collection of functions, but a clear representation of the resources the system manages.
Let's look at a typical Entity-Relationship Diagram (ERD) for a project management system.

As the diagram shows, a project is a central entity. It has a relationship with tasks, milestones, users, and clients. In RESTful design, we model these entities as resources, which are identified by URLs. The actions we perform on them (Create, Read, Update, Delete) are mapped to standard HTTP methods.
To learn the foundational principles of this process, the following video provides an excellent overview of how to design APIs like a senior engineer, focusing on the core concepts we'll be using today.
How to Design APIs Like a Senior Engineer (REST, GraphQL, Auth, Security)
This segment from Hayk Simonyan's channel explains how to model business domains into RESTful resources. It covers best practices for URL design, including the use of plural nouns and handling nested resources.
Please watch the section on resource modeling and URL design from the beginning. Pay close attention to how products and reviews are modeled as collections and how their relationship is expressed through a nested URL.
As you just saw, the key is to use plural nouns for collections (e.g., /projects) and to represent parent-child relationships through nested paths (e.g., /projects/{project_id}/tasks).
The Three Pillars of API Design Decisions
When designing these endpoints, our decisions should be driven by three factors we established during the requirements phase:
- Access Patterns: How will the frontend client query this data? An API to fetch all tasks for a single project (
GET /projects/{id}/tasks) is a clear access pattern. - Data Volume: How much data are we dealing with? For lists of projects or tasks, we must design for pagination from day one to handle potentially large datasets efficiently.
- Consistency Requirements: Our use of foreign keys in the database ensures referential integrity (a task must belong to a valid project). Our API design must honor and expose these relationships logically.
With these principles in mind, let's design the API contracts.
Low-Level Design: Project Management Service
We will organize our endpoints logically around the core resources: projects, tasks, and milestones. All endpoints are prefixed with /api/v1 and are protected by our authentication and RBAC dependencies. The tenant_id is always derived from the user's JWT, never accepted from a request body or URL parameter.
1. Projects Resource
A project is the top-level container for all related work.
| Action | Method & Endpoint | Description |
|---|---|---|
| List Projects | GET /projects |
Retrieves a paginated list of projects for the current tenant. Supports filtering and sorting. |
| Create Project | POST /projects |
Creates a new project within the current tenant's scope. |
| Get Project | GET /projects/{project_id} |
Retrieves a single project by its ID. |
| Update Project | PATCH /projects/{project_id} |
Partially updates a project's details (e.g., name, status). |
| Delete Project | DELETE /projects/{project_id} |
Deletes a project and its associated data. |
Data Flow & API Contracts:
-
GET /projects?status=active&sort=start_date_desc&page=1&limit=20- Description: This endpoint showcases how we'll handle real-world data retrieval.
- Permissions: User needs
can_view_projectspermission. - Data Flow:
- Auth dependency validates the JWT and provides the
tenant_id. - RBAC dependency checks for
can_view_projects. - The service layer queries the
projectstable withWHERE tenant_id = ?. - Filtering (
status='active'), sorting (ORDER BY start_date DESC), and pagination (LIMIT 20 OFFSET 0) are applied to the query. - The response includes the list of projects and pagination metadata.
- Auth dependency validates the JWT and provides the
- Success Response (200 OK):
{ "items": [ { "id": "proj_uuid_1", "name": "Skyscraper Foundation", "start_date": "2024-10-01", "status": "active" } ], "total": 1, "page": 1, "limit": 20 }
-
POST /projects- Permissions: User needs
can_create_projectspermission. - Request Body:
{ "name": "New Office Building", "description": "20-story office building construction.", "start_date": "2025-01-15" } - Data Flow: The service layer validates the payload, creates a new project record, and associates it with the
tenant_idfrom the JWT context. - Success Response (201 Created):
{ "id": "proj_uuid_2", "name": "New Office Building", "description": "20-story office building construction.", "start_date": "2025-01-15", "status": "pending", "created_at": "2024-09-15T10:00:00Z" } - Error Responses:
403 Forbidden(insufficient permissions),422 Unprocessable Entity(validation error).
- Permissions: User needs
2. Tasks Resource
Tasks are children of projects. This relationship is reflected in the API design. We'll use a nested route for creating/listing tasks within a project context and flat routes for operating on a specific task.
| Action | Method & Endpoint | Description |
|---|---|---|
| List Tasks in Project | GET /projects/{project_id}/tasks |
Retrieves a list of tasks for a specific project. |
| Create Task in Project | POST /projects/{project_id}/tasks |
Creates a new task within a specific project. |
| Get Task | GET /tasks/{task_id} |
Retrieves a single task by its ID. |
| Update Task | PATCH /tasks/{task_id} |
Partially updates a task (e.g., status, assignee). |
| Delete Task | DELETE /tasks/{task_id} |
Deletes a task. |
Data Flow & API Contracts:
-
POST /projects/{project_id}/tasks- Permissions: User needs
can_create_taskspermission. - Data Flow:
- The service first verifies that
{project_id}belongs to the current user's tenant to prevent cross-tenant data creation. - A new task record is created with a foreign key to
{project_id}and thetenant_id.
- The service first verifies that
- Request Body:
{ "name": "Lay the concrete foundation", "due_date": "2025-02-01", "assignee_id": "user_uuid_xyz" // Optional: ID of a user in the same tenant } - Success Response (201 Created): The full task object.
- Permissions: User needs
-
PATCH /tasks/{task_id}- Permissions: User needs
can_edit_tasks. If changing assignee, perhapscan_assign_tasks. This demonstrates how permissions can get granular. - Data Flow:
- The service fetches the task by
{task_id}, ensuring it belongs to the user'stenant_id. - The provided fields are updated.
- The service fetches the task by
- Request Body:
{ "status": "in_progress" // Example of updating a task's state } - Success Response (200 OK): The updated task object.
- Permissions: User needs
3. Milestones Resource
Milestones are significant checkpoints in a project. Their API design mirrors that of tasks.
| Action | Method & Endpoint | Description |
|---|---|---|
| List Milestones | GET /projects/{project_id}/milestones |
Retrieves milestones for a specific project. |
| Create Milestone | POST /projects/{project_id}/milestones |
Creates a new milestone for a project. |
| Get Milestone | GET /milestones/{milestone_id} |
Retrieves a single milestone by its ID. |
| Update Milestone | PATCH /milestones/{milestone_id} |
Updates a milestone's details. |
| Delete Milestone | DELETE /milestones/{milestone_id} |
Deletes a milestone. |
Data Flow & API Contracts:
- The contracts for milestones are analogous to tasks. For example,
POST /projects/{project_id}/milestoneswould create a milestone linked to that project. - A key feature for milestones might be automatic status updates based on task completion. This logic would reside in the service layer, triggered when a task's status changes. The API contract itself remains simple.
- Request Body for
POST:{ "name": "Foundation Phase Complete", "due_date": "2025-03-01" } - Success Response (201 Created): The new milestone object.
A Design That Breathes: Best Practices
This design follows several best practices that make an API robust and pleasant to work with.
REST API Design: Endpoints, Resources and Data Patterns
This article from Medium provides a comprehensive workflow for API design. It reinforces the concepts we've applied today.
Please review the following sections: The summarized workflow provides a great checklist for our process. Read about specifying resource attributes, which corresponds to our request/response body definitions. The section on nested resources justifies our hybrid approach for tasks (nested for listing, flat for single operations). Finally, review the section on implementing pagination, filtering, and sorting, which we've built into our GET endpoints for collections.
By defining clear resources, using standard HTTP methods, and planning for real-world use cases like pagination, we create an API that is not only functional but also scalable and maintainable.
Conclusion
We have successfully created a detailed low-level design for our project management service. This design translates our business requirements into concrete API contracts that are ready for implementation. We have defined the resources, their endpoints, the shape of the data they exchange, and the underlying data flows that respect our multi-tenancy and RBAC rules.
Key Takeaways:
- Resource-Oriented Design: We mapped our core database entities (
projects,tasks,milestones) to RESTful resources using plural nouns. - Hierarchical & Flat Routes: We used nested routes (
/projects/{id}/tasks) to reflect parent-child relationships for listing/creation and flat routes (/tasks/{id}) for efficient operations on individual resources. - Tenant Isolation by Default: All API data flows are implicitly scoped to the
tenant_idextracted from the JWT, ensuring strict data separation. - Practical API Contracts: Our list endpoints are designed with pagination, sorting, and filtering from the start, which is essential for performance and a good user experience.
Preview of the Next Lesson:
With the system architecture and low-level API design complete for our core services, we have a solid blueprint. It's time to move from design to the database. In the next module, we will begin Phase 2: Multi-Tenant Database Design, where we'll translate these designs into a concrete PostgreSQL schema, starting with the tables for tenants and subscriptions.