Streaming is an interesting topic in software engineering. Whether it is about music, video, or just simple data, applying this concept from a design, architecture, and coding perspective can become quickly complex if not thought through correctly.
In today's article, we will build a service to stream music using Golang and React. We will start by showing the incremental system design along with the corresponding code implementation for each step. By the end of this series of articles, you will learn:
How to build an API using Python and Golang
How to serve HTTP range requests
How to create a system design/architecture for a music streaming service
Without further ado, let's dive into the project.
If you are interested in more content covering topics like this, subscribe to my newsletter for regular updates on software programming, architecture, and tech-related insights.
Introduction to the Project
Building a music streaming service is a challenging yet rewarding exercise for developers, and I am happy to see you here following along with me. For simplicity, we will only focus on implementing the streaming part of the solution. Thus, there is no need to have an API service or other additional services at this stage.
Here are the requirements for the system design:
Basic Song Retrieval: The client requests a song, and the server responds with a JSON object containing the song URL.
Scalability: Efficiently handle thousands of requests with ease.
Secure Streaming: Prevent direct downloads and serve data using HTTP range requests.
Global Accessibility: Ensure low latency, reliability, redundancy, and security worldwide.
With these requirements in mind, let's create the codebase for this article to keep updating the codebase according to the changes implemented.
Setup the Project
For this project, we are using Django, Golang, and React. Golang provides an easy way to write performant code, while React has a lot of libraries that can quickly help us set up a frontend for this project. Django will be used for building the API that serves data about the songs in the database.
You can skip this part if you do not have enough time. Just make sure to clone the project and switch to the base
branch.
git clone -b base https://github.com/koladev32/golang-react-music-streaming.git
cd golang-react-music-streaming
make setup
If you prefer to set it up manually, follow this guide.
In your work folder, create a folder called backend
. This folder will contain the code for the Django API we'll build.
cd backend
python3 -m venv venv
source venv/bin/activate
Then, let's install the required libraries and create a Django project.
pip install django djangorestframework pillow django-cors-headers
Django and Django REST Framework: These will be used to build the song model and serve the JSON information from a REST API.
Django Cors Headers: This will help us set up CORS configuration so the frontend can make easy requests to the backend.
Pillow: This is used as we are going to serve media files and static files (thumbnails) from the API.
Make sure these dependencies are registered in the requirements.txt
file.
Django==5.0.7
djangorestframework==3.15.2
pillow==10.4.0
django-cors-headers==4.4.0
Now, create a project called backend
, and then a new Django application called music
.
django-admin startproject backend .
django-admin startapp music
Once this is done, run the database migrations and start the Django server. It should be running at http://localhost:8000.
python manage.py migrate
python manage.py runserver
Setting Up the Frontend
At the root of the project, type the following command to create a new Next.js project.
npx create-next-app@latest
After running this command, you will be presented with a prompt with different options to choose from. Here are the options I've chosen.
Once it is done, enter the frontend directory and install a media player package, react-h5-audio-player
.
npm install react-h5-audio-player
react-h5-audio-player
is a customizable and accessible audio player component for React applications. It provides essential playback controls and leverages the HTML5 audio element for reliable performance. This will be useful for this project as we aim for simplicity.
Once it is done, run the frontend server with the following command:
npm run dev
This will start a Next.js application at http://localhost:3000.
With the base project setup, we can start writing the code for the first version of the application.
Basic Song Retrieval
In the previous section, we set up the project. In this section, we will build the MVP of the product, an application that can do a basic song retrieval and play it through a web frontend.
Before doing that, let's visualize the system design for this requirement.
The system design involves clients requesting data about songs with access to the storage of the songs files, to retrieve them. The API processes these requests, interacting with a database that stores song metadata and file storage for audio files.
This system design ensures the following characteristics:
Scalability: This is a simple monolithic application. Replicating the same instance of this architecture across many regions and many times to ensure reliability and using a load balancer to ensure traffic distribution is a way to go. Naturally, in our case, we are focusing on an MVP.
Storage: The storage is directly on the server. This ensures quick writing when creating a song object in the database for example.
Let's now write the coding implementation.
Building the Backend
In the MVP, the backend will only serve an API capable of retrieving and listing songs. Let's start by writing the Song
model.
In the music/
models.py
file, create a new model called Song
with the following fields: title, author, duration, thumbnail, and file.
from django.db import models
class Song(models.Model):
name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
duration = models.IntegerField()
thumbnail = models.ImageField(upload_to='images/')
file = models.FileField(upload_to='music/')
This model defines the basic structure for our song data. Each song has a name, an artist, a duration (in seconds), a thumbnail image, and an audio file.
For this project, I am using songs from the https://freemusicarchive.org/ website. Add this script at the root of the backend project, in the same scope as manage.py
.
# insert_songs.py
import os
import django
# Set up the Django environment
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
django.setup()
from music.models import Song
import requests
from django.core.files.base import ContentFile, File
from django.core.files.temp import NamedTemporaryFile
# List of songs to be inserted
songs = [
{
"name": "Irreplaceable",
"author": "Sapio",
"file": "https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/BYl1B50WKQCKbiLtECjNYumY18htQTlaqJ5MpUTt.mp3?download=1&name=Sapio%20-%20Irreplaceable.mp3",
"thumbnail": "https://freemusicarchive.org/image/?file=track_image%2Fvu1EU9H0Pxv67II9pAOvQ28fHdAZppapGOQgYbxY.png&width=290&height=290&type=track",
"duration": 300
},
{
"name": "Soulmates (Dear Future Wife / Husband)",
"author": "Tadz",
"file": "https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/vGwJswHSC5hO8wmqNvVKpoE9BaehzkkiBWLMASR7.mp3?download=1&name=Tadz%20-%20Soulmates%20%28Dear%20Future%20Wife%20%2F%20Husband%29.mp3",
"thumbnail": "https://freemusicarchive.org/image/?file=track_image%2FVn4FvKcfzVnLfiQ4zRqarP18oSfQ19o3gG1FfynR.png&width=290&height=290&type=track",
"duration": 300
},
{
"name": "High School Crush",
"author": "Tadz",
"file": "https://files.freemusicarchive.org/storage-freemusicarchive-org/tracks/3Y0b1YV4ePQ0ZwEfMUmDXOtvArQz4Nsoe2rO777W.mp3?download=1&name=Tadz%20-%20High%20School%20Crush.mp3",
"thumbnail": "https://freemusicarchive.org/image/?file=track_image%2F0dyea9xG9zypHMyiVJ58CyQQWT8Ena2JWZKB0QH0.jpg&width=290&height=290&type=track",
"duration": 317
}
]
def download_file(url):
response = requests.get(url)
if response.status_code == 200:
return ContentFile(response.content)
else:
return None
for song_data in songs:
song = Song(
name=song_data['name'],
artist=song_data['author'],
duration=song_data['duration']
)
# Download and save the thumbnail
thumbnail = download_file(song_data['thumbnail'])
if thumbnail:
temp_thumb = NamedTemporaryFile(delete=True)
temp_thumb.write(thumbnail.read())
temp_thumb.flush()
song.thumbnail.save(f"{song_data['name']}_thumbnail.jpg", File(temp_thumb))
# Download and save the song file
song_file = download_file(song_data['file'])
if song_file:
temp_file = NamedTemporaryFile(delete=True)
temp_file.write(song_file.read())
temp_file.flush()
song.file.save(f"{song_data['name']}.mp3", File(temp_file))
song.save()
print(f"Inserted {song.name} by {song.artist}")
To run the script, use the following command.
python populate.py
Now you have data in your database and we can proceed with the tutorial.
Next, we need to create a serializer. In the music
directory, create a file called serializers.py
and add the following code.
from rest_framework import serializers
from .models import Song
class SongSerializer(serializers.ModelSerializer):
class Meta:
model = Song
fields = '__all__'
A serializer in Django REST Framework is responsible for converting complex data types, such as querysets and model instances, into native Python datatypes that can be easily rendered into JSON, XML, or other content types. Here, SongSerializer
converts the Song
model into JSON format.
Then, create another file called viewsets.py
where we will add the request logic handler for the songs.
from rest_framework import viewsets, permissions
from music.models import Song
from music.serializers import SongSerializer
class SongViewSet(viewsets.ModelViewSet):
queryset = Song.objects.all()
serializer_class = SongSerializer
permission_classes = [permissions.AllowAny]
In this code, we are declaring a viewset called SongViewSet
and setting the default queryset to Song.objects.all()
. This means that every listing or retrieving action will use this queryset to return the desired list of objects or the object itself. We are also setting the serializer class and the permission classes. For simplicity, we are allowing anyone to interact with the API without authentication and permissions, but in a real-world project, the API should be protected via authentication.
Now that we have written the API logic for the music application, let's register the viewsets in the URLs of the Django application by creating a router. We will then proceed to write some configurations in the settings.py
file of the project.
At the root of the Django project, create a file called routers.py
.
from rest_framework.routers import SimpleRouter
from music.viewsets import SongViewSet
router = SimpleRouter()
router.register('songs', SongViewSet)
urlpatterns = router.urls
In this code, we are creating a router using the SimpleRouter API. We are then registering a new route called songs
with the viewset concerned, SongViewSet
.
In the backend/
urls.py
file, add the following code to register the newly created router for our REST API:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include(("routers", "core"), namespace="music-api")),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
In the code above, we are registering a new path for the API and including the routes created in the routers.py
. The last line in this code snippet is important so we can work with
static and media files in development mode.
Now that we have added the URLs required to access the API, let's write some important configurations in the settings.py
file. These configurations will concern CORS, application configuration, and static and media file configuration.
...
# Application definition
INSTALLED_APPS = [
...
"corsheaders",
"rest_framework",
"music",
]
MIDDLEWARE = [
...
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
...
]
...
STATIC_URL = "static/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL = "/media/"
...
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
In the code above, we are registering new applications, such as corsheaders
, rest_framework
, and finally our defined app music
. In the next configuration, we are adding the CorsMiddleware
to the list of existing middleware. Then, we are adding configuration for static and media files by defining the STATIC_URL
, MEDIA_ROOT
, and finally MEDIA_URL
. Finally, we are adding the CORS_ALLOWED_ORIGINS
to set the only origins allowed to interact with the API. In this case, we are ensuring that applications running on localhost at port 3000 can freely access the API.
Great! Now run the following commands to create database migrations and apply them to the database.
python manage.py makemigrations
python manage.py migrate
Nice! With the backend and the API built and ready to serve data, we can now move to building the frontend of the application.
Building the Frontend
In the precedent section of this article, we have built the API to serve details about songs. In this section, we are going to build the frontend of the application using Next.js.
With Next.js, we are using the AppRouter
architecture which is pretty much straightforward. In the src
directory, create a new directory called components
. This directory will contain the components for this MVP application, such as the MusicPlayer
component. In this newly created directory, create a file called music-player.jsx
.
This file will contain the code for the MusicPlayer
component.
import React from 'react';
import AudioPlayer from 'react-h5-audio-player';
import 'react-h5-audio-player/lib/styles.css';
const MusicPlayer = ({ url }) => {
return (
<AudioPlayer
autoPlay
src={url}
onPlay={e => console.log("Playing")}
/>
);
};
export default MusicPlayer;
There is not much to see here, we are just using the AudioPlayer
component of react-h5-audio-player
and then passing the url
props that will be passed to the MusicPlayer
component. We are also importing the default CSS styles of react-h5-audio-player
.
Now, let's use this component in the page.js
file in the src/app
directory.
"use client"
import React, { useEffect, useState } from 'react';
import MusicPlayer from "@/components/music-player";
import Image from "next/image";
export default function Home() {
const [songs, setSongs] = useState([]);
const [currentSong, setCurrentSong] = useState(null);
useEffect(() => {
fetch('http://localhost:8000/api/songs/')
.then((response) => response.json())
.then((data) => setSongs(data));
}, []);
const playSong = (song) => {
setCurrentSong(song.file);
};
return (
<div className="container mx-auto px-4">
<h1 className="text-4xl font-bold text-center my-8">Music Streaming</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{songs.map((song) => (
<div key={song.id} className="bg-white p-4 rounded-lg shadow-md">
<Image width={400} height={400} src={song.thumbnail} alt={song.name} className="w-full h-48 object-cover rounded-lg mb-4" />
<h2 className="text-2xl font-semibold mb-2 text-black">{song.name}</h2>
<p className="text-gray-600 mb-2">By {song.artist}</p>
<button onClick={() => playSong(song)} className="text-blue-500 hover:underline">
Listen Now
</button>
</div>
))}
</div>
{currentSong && (
<div className="fixed bottom-0 left-0 right-0 bg-gray-800 p-4">
<MusicPlayer url={currentSong} />
</div>
)}
</div>
);
}
Great! Now, in the frontend application, you will be able to display songs and listen to those songs when clicking on the "Listen Now" button.
We now have a working MVP of the application, but there are some concerns regarding the scalability and confidentiality of the songs.
Cons of Our Architecture
The system architecture for this MVP might be simple, but it has a lot of flaws, notably from a security and confidentiality standpoint. Here are the cons:
File Storage Distribution: File storage is directly on the server or next to the server. It must be distributed using a global CDN to ensure efficient file access.
Caching Requirements: The system doesn't have a caching component implemented. As the system is more of a read than a write system, we need to implement a caching component to avoid useless requests to the database.
These cons can be addressed as these only require additional setup and not too much code configuration. In the next part of this topic, we will configure caching with Redis but also configure storage with AWS S3. I will ensure to give the similar services or processes if you are using another cloud provider.
Conclusion
In this article, we have created the first version of our application where a user can request for songs details and then retrieve a file directly and start playing it in the browser. We have used Django to build the API, and then Next.js for the frontend.
In the second part of this article, we will add caching and a better distribution of the files using Redis and AWS S3.
If you enjoyed this article and want to stay updated with more content, subscribe to my newsletter. I send out a weekly or bi-weekly digest of articles, tips, and exclusive content that you won't want to miss 🚀