Luciano Spallettis side huffed and puffed for the majority of the contest but pulled ahead thanks to substitute Baldes brilliant strike six minutes after half-time.
The hosts had the chance to double their advantage when Bartłomiej Dragowski was controversially judged to have tripped Mauro Icardi in the box (cue some VAR is ruining football chat after a few minutes of waiting), but the Pole guessed correctly to thwart Icardis weak effort.
That penalty miss came back to haunt Inter when the unmarked Hamed Junior Traore tapped in at the back post from Salih Ucans low cross.
But Nainggolan was on hand to prod in after Matias Vecino struck the post to ease the tension at San Siro and secure another season of football in Europes elite club competition.
Inter had a late goal chalked off due to Keita Baldes tug on Dragowski after the goalkeeper went up for a corner, Marcelo Brozovic seeing his effort from the halfway line disallowed and Balde sent off.
Key Talking Point
Inter headed into Sundays final round of Serie A fixtures as one of three teams looking to qualify for next seasons Champions League. Following a late surge, Atalanta were third ahead of the final games, with Inter fourth and Milan fifth.
Atalanta fell behind early to Sassuolo on while Milan surged into a two goal lead at SPAL. Unsurprisingly, San Siro was a stadium packed with nerves as Nerazzurri supporters watched their side fire shots at Empolis goal with no success in the opening half hour, Dragowski being particularly impressive between the sticks.
Inter were heading for the Europa League at half-time, but Balde nudged the hosts back into the Champions League qualification spots just six minutes after his introduction from the bench, skipping past numerous challenges before arrowing a low shot past Dragowski.
After Icardi fluffed his lines from the penalty spot, things went from good to bad to terrible pretty quickly when 19-year-old Traore netted the equaliser. With both Atalanta and Milan winning, Inter needed another goal, and quickly.
Thankfully for Inter and Spalletti, Nainggolan popped up to bag the points and secure fourth place with his sixth league goal of the season before Baldes red card. At least its never dull following I Nerazzurri.
Starting XI: Handanovic (6); DAmbrosio (7), De Vrij (7), Skriniar (6), Asamoah (6); Vecino (7), Brozovic (7); Politano (7), Nainggolan (8), Perisic (6); Icardi (5).
Substitutes: Balde (8*), Martinez (6), Dalbert (6)
STAR MAN - Keita Balde gave Inter the injection of pace and flair on the ball when he was introduced after half-time and helped put Inters fans at ease momentarily with his great goal.
The Senegal international breezed past a bunch of challenges before sending a low powerful effort past the outstretched arm of Dragowski.
Ok, he was sent off in the millionth minute of stoppage time, but Inter wouldnt have won without him.
Oh my word....Icardi misses from 12 yards!But we lead courtesy of Keita Baldes cameo strike.We shall be in the Champions League come what may.#InterEmpoli
— Pedro Onyeka (@pedronyeka) May 26, 2019
Luciano Spallettis gets it absolutely right as his 2nd half sub Keita Balde scores for Inter!
— Matteo Bonetti (@TheCalcioGuy) May 26, 2019
INTER LEAD ITS KEITA BALDE!!! How important could that be at both ends of the table!!! (1-0)
— Football Chatters (@FootballChat555) May 26, 2019
Starting XI: Dragowski (8); Maietta (5), Silvestre (6), DellOrco (6); Bennacer (6); Lorenzo (6), Acquah (5), Traore (7), Pajac (6); Farias (6), Caputo (5).Substitutes: Brighi (6), Ucan (7).
With the Serie A season now finished and Champions League football to look forward to next term, Inter will look to rebuild over the summer, potentially with a new manager with Spallettis future still uncertain.
Empoli will play in Serie B next season after Genoa managed to avoid defeat away at Fiorentina.