A modern, frontend-friendly Star Wars API built with Next.js 16 and TypeScript.
π Live Demo: https://sw-next-api.vercel.app
π Π ΡΡΡΠΊΠ°Ρ Π²Π΅ΡΡΠΈΡ: README.ru.md
All API endpoints are relative. Prepend your base URL:
- Local development:
http://localhost:3000 - Production (example):
https://sw-next-api.vercel.app
Example: GET /api/v1/people/1 becomes GET http://localhost:3000/api/v1/people/1
This API exists because most Star Wars APIs are data-oriented, not UI-oriented.
Traditional APIs like SWAPI require multiple requests to display a single UI screen. This project demonstrates how the same data can be exposed in a frontend-friendly way using the Backend for Frontend (BFF) pattern.
To display a character with their homeworld and films:
SWAPI approach:
GET /people/1 # Luke Skywalker
GET /planets/1 # Tatooine
GET /films/1 # A New Hope
GET /films/2 # Empire Strikes Back
GET /films/3 # Return of the Jedi
# ... 5+ requests for one character
This API:
GET /api/v1/people/1?expand=homeworld,films
# 1 request, all data included
Query exactly what you need:
# Just basic data (minimal projection)
GET /api/v1/people/1
# Include homeworld
GET /api/v1/people/1?expand=homeworld
# Multiple levels (depth limited to 2)
GET /api/v1/people/1?expand=films.charactersGET /api/v1/people?page=2&limit=10
Response:
{
"page": 2,
"limit": 10,
"total": 82,
"pages": 9,
"results": [...]
}# Search by name
GET /api/v1/people?search=luke
# Filter by attributes
GET /api/v1/people?gender=female
# Sort results
GET /api/v1/people?sort=-name # descending
GET /api/v1/films?sort=episode # ascendingGET /api/v1/people?search=skywalker&gender=male&sort=name&page=1&limit=5&expand=homeworldUnlike SWAPI, this API includes additional metadata:
People:
isForceUser- Force-sensitive charactersisJedi/isSith- Force alignmentfaction- rebels, empire, republic, separatists, civilian
Starships:
is_military- Military vs civilian classificationfaction- Which faction operates the vessel
# Find all Jedi
GET /api/v1/people?isJedi=true
# Find Imperial military ships
GET /api/v1/starships?faction=empire&is_military=trueGET /api/v1/people/{id}GET /api/v1/films/{id}GET /api/v1/planets/{id}GET /api/v1/species/{id}GET /api/v1/starships/{id}GET /api/v1/vehicles/{id}
GET /api/v1/peopleGET /api/v1/filmsGET /api/v1/planetsGET /api/v1/speciesGET /api/v1/starshipsGET /api/v1/vehicles
Film is the central entity connecting all other entity types. Here's the complete relationship structure:
βββββββββββββββββββββββββββββββββββββββ
β FILM β
β (Central Hub - All relationships) β
ββββββββββββ¬ββββββ¬ββββββ¬ββββββββββββββ
β β β
ββββββββββββββββββββββ β β
β β β
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ βββββββββββββββ
β PERSON βββββββββββββββ€ PLANET β β SPECIES β
ββββββ¬ββββββ ββββββββ¬ββββ βββββββ¬ββββββββ
β β β
β βββββββββββββββββββ¬ββββ βββββββ
β β β β
βββββββββββββΌββββΌββββββββββββββ β βββββββ΄ββββββββ
β β β β β β β
βΌ βΌ βΌ βΌ βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββββββ βββββββββββββββ
β STARSHIP β β VEHICLE β β (Homeworld) β β (Pilots) β
ββββββββββββ ββββββββββββ ββββββββββββββββ βββββββββββββββ
| From β To | Type | Bidirectional | Depth Limit |
|---|---|---|---|
| Film β Person | many-to-many | β | 2 |
| Film β Planet | many-to-many | β | 2 |
| Film β Species | many-to-many | β | 2 |
| Film β Starship | many-to-many | β | 2 |
| Film β Vehicle | many-to-many | β | 2 |
| Person β Planet (homeworld) | many-to-one | β | 2 |
| Person β Species | many-to-many | β | 2 |
| Person β Starship | many-to-many | β | 2 |
| Person β Vehicle | many-to-many | β | 2 |
| Species β Planet (homeworld) | many-to-one | β | 2 |
Key Points:
- Maximum expansion depth: 2 levels (prevents infinite loops)
- Normalized responses: Related entities return as minimal objects (
{entityId, id, name}) - Type-safe: All relationships use
EntityIdfor type safety - Efficient: Prevents over-fetching with controlled expansion
// GET /api/v1/films/1?expand=characters.homeworld
{
"entityId": 1,
"title": "A New Hope",
"characters": [ // Depth 1
{
"entityId": 1,
"name": "Luke Skywalker",
"homeworld": { // Depth 2 (stops here)
"entityId": 1,
"name": "Tatooine"
}
}
]
}βββββββββββββββββββ
β API Routes β Next.js API handlers
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β BFF Layer β Aggregates related data
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β Projections β Transforms to response models
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β Expand System β Controlled depth expansion
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β Resolver β Universal entity lookup
ββββββββββ¬βββββββββ
β
ββββββββββΌβββββββββ
β Normalized β In-memory data store
β Data β
βββββββββββββββββββ
BFF (Backend for Frontend):
- Aggregates multiple entities in one request
- Returns UI-ready data structures
- Eliminates waterfall requests
Controlled Expansion:
- Whitelist of expandable fields per entity
- Depth limit prevents graph explosion
- Type-safe expansion tree
Universal Resolver:
- Single lookup function for all entities
- Supports both EntityId and slug
- Centralized mapping registry
Response Projections:
- Fixed contracts for API responses
- Minimal projections for related entities
- Prevents over-fetching
Normalized Data:
- camelCase naming (heightCm, birthYearBBY)
- Proper types (numbers instead of strings)
- Minimal objects for relationships
# Install dependencies
npm install
# Run development server
npm run dev
# Build for production
npm run build
# Run production server
npm startconst response = await fetch(
'http://localhost:3000/api/v1/people/1?expand=homeworld,films'
);
const luke = await response.json();
console.log(luke.name); // "Luke Skywalker"
console.log(luke.homeworld.name); // "Tatooine"
console.log(luke.films.length); // 4const response = await fetch(
'http://localhost:3000/api/v1/people?search=skywalker&page=1&limit=5'
);
const data = await response.json();
console.log(data.total); // 3
console.log(data.results.length); // 3
console.log(data.results[0].name); // "Anakin Skywalker"const response = await fetch(
'http://localhost:3000/api/v1/people?isJedi=true&faction=republic&expand=homeworld&limit=10'
);
const jedi = await response.json();
// All Jedi from the Republic with their homeworlds- Next.js 16 - App Router with React Server Components
- TypeScript - Full type safety across the stack
- Tailwind CSS 4 - Modern styling with CSS-first approach
- In-memory data - Fast, no database needed
- Vercel - Serverless deployment
- β JSON-LD structured data for rich snippets
- β OpenGraph & Twitter cards
- β Dynamic OG image generation
- β Canonical URLs
- β Sitemap.xml
- β Robots.txt (API endpoints excluded from indexing)
MIT
Repository: github.com/maiano/sw-api
Author: maiano