Skip to content

Commit 19b57e9

Browse files
authored
feat: add movie creation endpoint (#25)
1 parent 430b623 commit 19b57e9

46 files changed

Lines changed: 2757 additions & 77 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ dist/
55
build/
66
out/
77
.artifacts/
8+
artifacts/
89
.dotnet-cli-home/
910
restore*.log
1011
*.log
1112

1213
# .NET
14+
packages/
1315
*.user
1416
*.rsuser
1517
*.suo
@@ -50,3 +52,5 @@ docker-compose.override.local.yml
5052
Thumbs.db
5153
Desktop.ini
5254
.DS_Store
55+
*.tmp
56+
*.swp
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# GitHub Issue Context
2+
3+
## Source
4+
5+
- Repository: marciomyst/SmartMovieCatalog
6+
- Issue: #11
7+
- URL: https://github.com/marciomyst/SmartMovieCatalog/issues/11
8+
- State: OPEN
9+
- Created: 05/02/2026 22:05:54
10+
- Updated: 05/02/2026 22:25:51
11+
- Milestone: M1 — Core Movie Catalog
12+
13+
## Title
14+
15+
Create movie
16+
17+
## Labels
18+
19+
- type:feature
20+
- priority:high
21+
- area:api
22+
- area:backend
23+
- area:catalog
24+
- v1
25+
26+
## Assignees
27+
28+
- marciomyst
29+
30+
31+
32+
## Issue Body
33+
34+
## Summary
35+
36+
Implement the first real product vertical slice for creating a movie in Smart Movie Catalog.
37+
38+
This issue replaces part of the scaffold direction with an actual movie catalog behavior, while keeping the implementation intentionally simple and aligned with the current Clean Architecture structure.
39+
40+
## Goal
41+
42+
Allow a user to create a movie with basic metadata through the application.
43+
44+
## Scope
45+
46+
- Add a create movie API endpoint.
47+
- Define explicit request/response DTOs in `SmartMovieCatalog.Contracts`.
48+
- Add minimal application use case/orchestration in `SmartMovieCatalog.Application`.
49+
- Add minimal domain model or domain representation only if required by the behavior.
50+
- Persist or store the movie using the currently accepted persistence approach.
51+
- Return a stable response containing the created movie identifier and main movie data.
52+
- Add/update basic frontend flow if the movie creation page already exists or is part of this slice.
53+
- Update documentation if API contracts or architecture behavior changes.
54+
55+
## Suggested API
56+
57+
```http
58+
POST /api/movies
59+
60+
{
61+
"title": "Central do Brasil",
62+
"originalTitle": "Central do Brasil",
63+
"releaseYear": 1998,
64+
"countryCode": "BR",
65+
"originalLanguage": "pt-BR",
66+
"genres": ["Drama"],
67+
"director": "Walter Salles",
68+
"synopsis": "A retired teacher and a young boy travel through Brazil in search of his father.",
69+
"durationMinutes": 110,
70+
"ageRating": "12"
71+
}
72+
```
73+
74+
75+
### Acceptance Criteria
76+
77+
- A movie can be created through the API.
78+
- The endpoint returns `201 Created` when creation succeeds.
79+
- The response includes the created movie ID.
80+
- Required fields are validated.
81+
- Invalid input returns a consistent validation/error response.
82+
- Business rules do not live directly in controllers.
83+
- API contracts do not expose persistence models.
84+
- The implementation respects Clean Architecture dependency direction.
85+
- No authentication is required in this issue.
86+
- No Gemini, SignalR, CQRS, Wolverine, RAG, semantic search, or event-driven behavior is introduced.
87+
88+
### Technical Notes
89+
90+
- Keep the first implementation small.
91+
- Prefer explicit DTOs.
92+
- Do not introduce speculative abstractions.
93+
- If persistence is not fully implemented yet, this issue depends on the persistence foundation decision or must use a clearly documented temporary storage approach.
94+
- Do not leak database-specific details through API responses.
95+
96+
### Out of Scope
97+
98+
- Poster upload.
99+
- Gemini Vision analysis.
100+
- SignalR notifications.
101+
- Authentication/authorization.
102+
- Semantic search.
103+
- Advanced duplicate detection.
104+
- Bulk import.
105+
- TMDb integration.
106+
107+
## Comments
108+
109+
_No comments_
110+
111+
## Instructions for Spec Kit
112+
113+
Use this GitHub issue as the primary source of truth.
114+
115+
Convert the issue into a Spec Kit feature specification before creating the implementation plan.
116+
117+
Preserve:
118+
119+
- business goal;
120+
- user stories;
121+
- acceptance criteria;
122+
- technical constraints;
123+
- non-goals;
124+
- dependencies;
125+
- open questions.
126+
127+
If information is missing, add it under a clearly marked **Clarifications Needed** section instead of inventing requirements.
128+
129+
If the issue conflicts with existing project documentation, explicitly call out the conflict.
130+
131+
Prefer a small, incremental implementation plan aligned with the repository's existing architecture, folder structure, language, framework, and conventions.

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Do not read, modify, or base analysis on generated/vendor output unless explicit
127127
- `test:`
128128

129129
<!-- SPECKIT START -->
130-
Current Spec Kit plan: `specs/023-authentication-login-screen/plan.md`.
130+
Current Spec Kit plan: `specs/011-create-movie/plan.md`.
131131

132132
Before using Spec Kit skills, read `.specify/memory/constitution.md`.
133133
If the spec, plan, or implementation touches backend, API, contracts, domain,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using SmartMovieCatalog.Api.Common;
3+
using SmartMovieCatalog.Application.Features.Movies;
4+
using SmartMovieCatalog.Contracts.Movies;
5+
using Wolverine;
6+
7+
namespace SmartMovieCatalog.Api.Features.Movies.CreateMovie;
8+
9+
public static class CreateMovieEndpoint
10+
{
11+
public static void MapCreateMovie(this IEndpointRouteBuilder app)
12+
{
13+
app.MapPost("/api/movies", HandleAsync)
14+
.WithName("CreateMovie")
15+
.WithTags("Movies")
16+
.AllowAnonymous()
17+
.AddEndpointFilter<ValidationFilter<CreateMovieRequest>>()
18+
.Produces<MovieResponse>(StatusCodes.Status201Created)
19+
.ProducesValidationProblem(StatusCodes.Status400BadRequest);
20+
}
21+
22+
private static async Task<IResult> HandleAsync(
23+
[FromBody] CreateMovieRequest? request,
24+
IMessageBus messageBus,
25+
CancellationToken cancellationToken)
26+
{
27+
ArgumentNullException.ThrowIfNull(request);
28+
29+
CreatedMovie createdMovie = await messageBus.InvokeAsync<CreatedMovie>(
30+
new CreateMovieCommand(
31+
request.Title!,
32+
request.OriginalTitle,
33+
request.ReleaseYear!.Value,
34+
request.CountryCode!,
35+
request.OriginalLanguage!,
36+
request.Genres,
37+
request.Director,
38+
request.Synopsis,
39+
request.DurationMinutes,
40+
request.AgeRating),
41+
cancellationToken);
42+
43+
MovieResponse response = new(
44+
createdMovie.Id.ToString(),
45+
createdMovie.Title,
46+
createdMovie.OriginalTitle,
47+
createdMovie.ReleaseYear,
48+
createdMovie.CountryCode,
49+
createdMovie.OriginalLanguage,
50+
createdMovie.Genres,
51+
createdMovie.Director,
52+
createdMovie.Synopsis,
53+
createdMovie.DurationMinutes,
54+
createdMovie.AgeRating);
55+
56+
return Results.Created($"/api/movies/{response.Id}", response);
57+
}
58+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using FluentValidation;
2+
using SmartMovieCatalog.Contracts.Movies;
3+
4+
namespace SmartMovieCatalog.Api.Features.Movies.CreateMovie;
5+
6+
public sealed class CreateMovieRequestValidator : AbstractValidator<CreateMovieRequest>
7+
{
8+
public CreateMovieRequestValidator()
9+
{
10+
RuleFor(request => request.Title)
11+
.NotEmpty();
12+
13+
RuleFor(request => request.ReleaseYear)
14+
.NotNull()
15+
.InclusiveBetween(1888, DateTimeOffset.UtcNow.Year + 1);
16+
17+
RuleFor(request => request.CountryCode)
18+
.NotEmpty()
19+
.Must(countryCode => countryCode is not null &&
20+
countryCode.Trim().Length == 2 &&
21+
countryCode.Trim().All(char.IsLetter))
22+
.WithMessage("Country code must contain exactly two letters.");
23+
24+
RuleFor(request => request.OriginalLanguage)
25+
.NotEmpty();
26+
27+
RuleForEach(request => request.Genres)
28+
.Must(genre => !string.IsNullOrWhiteSpace(genre))
29+
.WithMessage("Genre must not be empty.");
30+
31+
RuleFor(request => request.DurationMinutes)
32+
.GreaterThan(0)
33+
.When(request => request.DurationMinutes.HasValue);
34+
}
35+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using SmartMovieCatalog.Api.Features.Movies.CreateMovie;
2+
3+
namespace SmartMovieCatalog.Api.Features.Movies;
4+
5+
public static class MoviesEndpoints
6+
{
7+
public static void MapMovieEndpoints(this IEndpointRouteBuilder app)
8+
{
9+
app.MapCreateMovie();
10+
}
11+
}

backend/src/SmartMovieCatalog.Api/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
using SmartMovieCatalog.Api.Common;
33
using SmartMovieCatalog.Api.Features.Auth;
44
using SmartMovieCatalog.Api.Features.Auth.Authenticate;
5+
using SmartMovieCatalog.Api.Features.Movies;
6+
using SmartMovieCatalog.Api.Features.Movies.CreateMovie;
57
using SmartMovieCatalog.Application;
68
using SmartMovieCatalog.Contracts.Auth;
9+
using SmartMovieCatalog.Contracts.Movies;
710
using SmartMovieCatalog.Infrastructure;
811
using SmartMovieCatalog.Infrastructure.Persistence;
912
using Wolverine;
@@ -30,6 +33,7 @@ public static int Main(string[] args)
3033
builder.Services.AddApplication();
3134
builder.Services.AddInfrastructure(builder.Configuration);
3235
builder.Services.AddScoped<IValidator<AuthenticateRequest>, AuthenticateRequestValidator>();
36+
builder.Services.AddScoped<IValidator<CreateMovieRequest>, CreateMovieRequestValidator>();
3337
builder.Services.AddHealthChecks();
3438

3539
builder.Services.AddOpenApi();
@@ -69,6 +73,7 @@ public static int Main(string[] args)
6973

7074

7175
app.MapAuthEndpoints();
76+
app.MapMovieEndpoints();
7277

7378
app.MapHealthChecks("/health");
7479

backend/src/SmartMovieCatalog.Api/SmartMovieCatalog.Api.http

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,22 @@ Authorization: Bearer {{access_token}}
1616
Accept: application/json
1717

1818
###
19+
20+
POST {{SmartMovieCatalog.Api_HostAddress}}/api/movies
21+
Content-Type: application/json
22+
Accept: application/json
23+
24+
{
25+
"title": "Central do Brasil",
26+
"originalTitle": "Central do Brasil",
27+
"releaseYear": 1998,
28+
"countryCode": "br",
29+
"originalLanguage": "pt-BR",
30+
"genres": ["Drama"],
31+
"director": "Walter Salles",
32+
"synopsis": "A retired teacher and a young boy travel through Brazil in search of his father.",
33+
"durationMinutes": 110,
34+
"ageRating": "12"
35+
}
36+
37+
###
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using SmartMovieCatalog.Domain.Movies;
2+
3+
namespace SmartMovieCatalog.Application.Abstractions.Persistence;
4+
5+
public interface IMovieRepository
6+
{
7+
Task AddAsync(Movie movie, CancellationToken cancellationToken);
8+
9+
Task SaveChangesAsync(CancellationToken cancellationToken);
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace SmartMovieCatalog.Application.Features.Movies;
2+
3+
public sealed record CreateMovieCommand(
4+
string Title,
5+
string? OriginalTitle,
6+
int ReleaseYear,
7+
string CountryCode,
8+
string OriginalLanguage,
9+
IReadOnlyCollection<string>? Genres,
10+
string? Director,
11+
string? Synopsis,
12+
int? DurationMinutes,
13+
string? AgeRating);

0 commit comments

Comments
 (0)